You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sems/apps/sbc/CallLeg.cpp

1760 lines
54 KiB

/*
* Copyright (C) 2010-2011 Stefan Sayer
* Copyright (C) 2012-2013 FRAFOS GmbH
*
* This file is part of SEMS, a free SIP media server.
*
* SEMS 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.
*
* For a license to use the SEMS software under conditions
* other than those described here, or to purchase support for this
* software, please contact iptel.org by e-mail at the following addresses:
* info@iptel.org
*
* SEMS 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 "CallLeg.h"
#include "AmSessionContainer.h"
#include "AmConfig.h"
#include "ampi/MonitoringAPI.h"
#include "AmSipHeaders.h"
#include "AmUtils.h"
#include "AmRtpReceiver.h"
#include "SBCCallRegistry.h"
#define TRACE DBG
// helper functions
static const char *callStatus2str(const CallLeg::CallStatus state)
{
static const char *disconnected = "Disconnected";
static const char *disconnecting = "Disconnecting";
static const char *noreply = "NoReply";
static const char *ringing = "Ringing";
static const char *connected = "Connected";
static const char *unknown = "???";
switch (state) {
case CallLeg::Disconnected: return disconnected;
case CallLeg::Disconnecting: return disconnecting;
case CallLeg::NoReply: return noreply;
case CallLeg::Ringing: return ringing;
case CallLeg::Connected: return connected;
}
return unknown;
}
ReliableB2BEvent::~ReliableB2BEvent()
{
TRACE("reliable event was %sprocessed, sending %p to %s\n",
processed ? "" : "NOT ",
processed ? processed_reply : unprocessed_reply,
sender.c_str());
if (processed) {
if (unprocessed_reply) delete unprocessed_reply;
if (processed_reply) AmSessionContainer::instance()->postEvent(sender, processed_reply);
}
else {
if (processed_reply) delete processed_reply;
if (unprocessed_reply) AmSessionContainer::instance()->postEvent(sender, unprocessed_reply);
}
}
////////////////////////////////////////////////////////////////////////////////
// helper functions
enum HoldMethod { SendonlyStream, InactiveStream, ZeroedConnection };
static const string sendonly("sendonly");
static const string recvonly("recvonly");
static const string sendrecv("sendrecv");
static const string inactive("inactive");
static const string zero_connection("0.0.0.0");
/** returns true if connection is avtive.
* Returns given default_value if the connection address is empty to cope with
* connection address set globaly and not per media stream */
static bool connectionActive(const SdpConnection &connection, bool default_value)
{
if (connection.address.empty()) return default_value;
if (connection.address == zero_connection) return false;
return true;
}
enum MediaActivity { Inactive, Sendonly, Recvonly, Sendrecv };
/** Returns true if there is no direction=inactione or sendonly attribute in
* given media stream. It doesn't check the connection address! */
static MediaActivity getMediaActivity(const vector<SdpAttribute> &attrs, MediaActivity default_value)
{
// go through attributes and try to find sendonly/recvonly/sendrecv/inactive
for (std::vector<SdpAttribute>::const_iterator a = attrs.begin();
a != attrs.end(); ++a)
{
if (a->attribute == sendonly) return Sendonly;
if (a->attribute == inactive) return Inactive;
if (a->attribute == recvonly) return Recvonly;
if (a->attribute == sendrecv) return Sendrecv;
}
return default_value; // none of the attributes given, return (session) default
}
static MediaActivity getMediaActivity(const SdpMedia &m, MediaActivity default_value)
{
if (m.send) {
if (m.recv) return Sendrecv;
else return Sendonly;
}
else {
if (m.recv) return Recvonly;
}
return Inactive;
}
static bool isHoldRequest(AmSdp &sdp, HoldMethod &method)
{
// set defaults from session parameters and attributes
// inactive/sendonly/sendrecv/recvonly may be given as session attributes,
// connection can be given for session as well
bool connection_active = connectionActive(sdp.conn, false /* empty connection like inactive? */);
MediaActivity session_activity = getMediaActivity(sdp.attributes, Sendrecv);
for (std::vector<SdpMedia>::iterator m = sdp.media.begin();
m != sdp.media.end(); ++m)
{
if (m->port == 0) continue; // this stream is disabled, handle like inactive (?)
if (!connectionActive(m->conn, connection_active)) {
method = ZeroedConnection;
continue;
}
switch (getMediaActivity(*m, session_activity)) {
case Sendonly:
method = SendonlyStream;
continue;
case Inactive:
method = InactiveStream;
continue;
case Recvonly: // ?
case Sendrecv:
return false; // media stream is active
}
}
if (sdp.media.empty()) {
// no streams in the SDP, needed to set method somehow
if (!connection_active) method = ZeroedConnection;
else {
switch (session_activity) {
case Sendonly:
method = SendonlyStream;
break;
case Inactive:
method = InactiveStream;
break;
case Recvonly:
case Sendrecv:
method = InactiveStream; // well, no stream is something like InactiveStream, isn't it?
break;
}
}
}
return true; // no active stream was found
}
////////////////////////////////////////////////////////////////////////////////
// callee
CallLeg::CallLeg(const CallLeg* caller, AmSipDialog* p_dlg, AmSipSubscription* p_subs)
: AmB2BSession(caller->getLocalTag(),p_dlg,p_subs),
call_status(Disconnected),
on_hold(false),
hold(PreserveHoldStatus)
{
a_leg = !caller->a_leg; // we have to be the complement
set_sip_relay_only(false); // will be changed later on (for now we have no peer so we can't relay)
// enable OA for the purpose of hold request detection
if (dlg) dlg->setOAEnabled(true);
else WARN("can't enable OA!\n");
// code below taken from createCalleeSession
const AmSipDialog* caller_dlg = caller->dlg;
dlg->setLocalTag(AmSession::getNewId());
dlg->setCallid(AmSession::getNewId());
// take important data from A leg
dlg->setLocalParty(caller_dlg->getRemoteParty());
dlg->setRemoteParty(caller_dlg->getLocalParty());
dlg->setRemoteUri(caller_dlg->getLocalUri());
/* if (AmConfig::LogSessions) {
INFO("Starting B2B callee session %s\n",
getLocalTag().c_str());
}
MONITORING_LOG4(other_id.c_str(),
"dir", "out",
"from", dlg->local_party.c_str(),
"to", dlg->remote_party.c_str(),
"ruri", dlg->remote_uri.c_str());
*/
// copy common RTP relay settings from A leg
//initRTPRelay(caller);
vector<SdpPayload> lowfi_payloads;
setRtpRelayMode(caller->getRtpRelayMode());
setEnableDtmfTranscoding(caller->getEnableDtmfTranscoding());
caller->getLowFiPLs(lowfi_payloads);
setLowFiPLs(lowfi_payloads);
// A->B
SBCCallRegistry::addCall(caller_dlg->getLocalTag(),
SBCCallRegistryEntry(dlg->getCallid(), dlg->getLocalTag(), ""));
// B->A
SBCCallRegistry::addCall(dlg->getLocalTag(),
SBCCallRegistryEntry(caller_dlg->getCallid(), caller_dlg->getLocalTag(), caller_dlg->getRemoteTag()));
}
// caller
CallLeg::CallLeg(AmSipDialog* p_dlg, AmSipSubscription* p_subs)
: AmB2BSession("",p_dlg,p_subs),
call_status(Disconnected),
on_hold(false),
hold(PreserveHoldStatus)
{
a_leg = true;
// At least in the first version we start relaying after the call is fully
// established. This is because of forking possibility - we can't simply
// relay if we have one A leg and multiple B legs.
// It is possible to start relaying before call is established if we have
// exactly one B leg (i.e. no parallel fork happened).
set_sip_relay_only(false);
// enable OA for the purpose of hold request detection
if (dlg) dlg->setOAEnabled(true);
else WARN("can't enable OA!\n");
}
CallLeg::~CallLeg()
{
// do necessary cleanup (might be needed if the call leg is destroyed other
// way then expected)
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
i->releaseMediaSession();
}
while (!pending_updates.empty()) {
SessionUpdate *u = pending_updates.front();
pending_updates.pop_front();
delete u;
}
SBCCallRegistry::removeCall(getLocalTag());
}
void CallLeg::terminateOtherLeg()
{
if (call_status != Connected) {
DBG("trying to terminate other leg in %s state -> terminating the others as well\n", callStatus2str(call_status));
// FIXME: may happen when for example reply forward fails, do we want to terminate
// all other legs in such case?
terminateNotConnectedLegs(); // terminates all except the one identified by other_id
}
AmB2BSession::terminateOtherLeg();
// remove this one from the list of other legs
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
if (i->id == getOtherId()) {
i->releaseMediaSession();
other_legs.erase(i);
break;
}
}
// FIXME: call disconnect if connected (to put remote on hold)?
if (getCallStatus() != Disconnected) updateCallStatus(Disconnected); // no B legs should be remaining
}
void CallLeg::terminateNotConnectedLegs()
{
bool found = false;
OtherLegInfo b;
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
if (i->id != getOtherId()) {
i->releaseMediaSession();
AmSessionContainer::instance()->postEvent(i->id, new B2BEvent(B2BTerminateLeg));
}
else {
found = true; // other_id is there
b = *i;
}
}
// quick hack to remove all terminated entries from the list
other_legs.clear();
if (found) other_legs.push_back(b);
}
void CallLeg::removeOtherLeg(const string &id)
{
if (getOtherId() == id) AmB2BSession::clear_other();
// remove the call leg from list of B legs
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
if (i->id == id) {
i->releaseMediaSession();
other_legs.erase(i);
break;
}
}
/*if (terminate) AmSessionContainer::instance()->postEvent(id, new B2BEvent(B2BTerminateLeg));*/
}
// composed for caller and callee already
void CallLeg::onB2BEvent(B2BEvent* ev)
{
switch (ev->event_id) {
case B2BSipReply:
onB2BReply(dynamic_cast<B2BSipReplyEvent*>(ev));
break;
case ConnectLeg:
onB2BConnect(dynamic_cast<ConnectLegEvent*>(ev));
break;
case ReconnectLeg:
onB2BReconnect(dynamic_cast<ReconnectLegEvent*>(ev));
break;
case ReplaceLeg:
onB2BReplace(dynamic_cast<ReplaceLegEvent*>(ev));
break;
case ReplaceInProgress:
onB2BReplaceInProgress(dynamic_cast<ReplaceInProgressEvent*>(ev));
break;
case DisconnectLeg:
{
DisconnectLegEvent *dle = dynamic_cast<DisconnectLegEvent*>(ev);
if (dle) disconnect(dle->put_remote_on_hold, dle->preserve_media_session);
}
break;
case ResumeHeldLeg:
{
ResumeHeldEvent *e = dynamic_cast<ResumeHeldEvent*>(ev);
if (e) resumeHeld();
}
break;
case ChangeRtpModeEventId:
{
ChangeRtpModeEvent *e = dynamic_cast<ChangeRtpModeEvent*>(ev);
if (e) changeRtpMode(e->new_mode, e->media);
}
break;
case ApplyPendingUpdatesEventId:
if (dynamic_cast<ApplyPendingUpdatesEvent*>(ev)) applyPendingUpdate();
break;
case B2BSipRequest:
if (!sip_relay_only) {
// disable forwarding of relayed request if we are not connected [yet]
// (only we known that, the B leg has just delayed information about being
// connected to us and thus it can't set)
// Need not to be done if we have only one possible B leg so instead of
// checking call_status we can check if sip_relay_only is set or not
B2BSipRequestEvent *req_ev = dynamic_cast<B2BSipRequestEvent*>(ev);
if (req_ev) req_ev->forward = false;
}
// continue handling in AmB2bSession
default:
AmB2BSession::onB2BEvent(ev);
}
}
int CallLeg::relaySipReply(AmSipReply &reply)
{
std::map<int,AmSipRequest>::iterator t_req = recvd_req.find(reply.cseq);
if (t_req == recvd_req.end()) {
ERROR("Request with CSeq %u not found in recvd_req.\n", reply.cseq);
return 0; // ignore?
}
int res;
AmSipRequest req(t_req->second);
if ((reply.code >= 300) && (reply.code <= 305) && !reply.contact.empty()) {
// relay with Contact in 300 - 305 redirect messages
AmSipReply n_reply(reply);
n_reply.hdrs += SIP_HDR_COLSP(SIP_HDR_CONTACT) + reply.contact + CRLF;
res = relaySip(req, n_reply);
}
else res = relaySip(req, reply); // relay response directly
return res;
}
bool CallLeg::setOther(const string &id, bool forward)
{
if (getOtherId() == id) return true; // already set (needed when processing 2xx after 1xx)
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
if (i->id == id) {
setOtherId(id);
clearRtpReceiverRelay(); // release old media session if set
setMediaSession(i->media_session);
if (forward && dlg->getOAState() == AmOfferAnswer::OA_Completed) {
// reset OA state to offer_recived if already completed to accept new
// B leg's SDP
dlg->setOAState(AmOfferAnswer::OA_OfferRecved);
}
if (i->media_session) {
TRACE("connecting media session: %s to %s\n",
dlg->getLocalTag().c_str(), getOtherId().c_str());
i->media_session->changeSession(a_leg, this);
}
else {
// media session not set, set direct mode if not set already
if (rtp_relay_mode != AmB2BSession::RTP_Direct) setRtpRelayMode(AmB2BSession::RTP_Direct);
}
set_sip_relay_only(true); // relay only from now on
return true;
}
}
ERROR("%s is not in the list of other leg IDs!\n", id.c_str());
return false; // something wrong?
}
void CallLeg::b2bInitial1xx(AmSipReply& reply, bool forward)
{
// stop processing of 100 reply here or add Trying state to handle it without
// remembering other_id (for now, the 100 won't get here, but to be sure...)
// Warning: 100 reply may have to tag but forward is explicitly set to false,
// so it can't be used to check whether it is related to a forwarded request
// or not!
if (reply.to_tag.empty() || reply.code == 100) return;
if (call_status == NoReply) {
DBG("1xx reply with to-tag received in NoReply state,"
" changing status to Ringing and remembering the"
" other leg ID (%s)\n", getOtherId().c_str());
if (setOther(reply.from_tag, forward)) {
updateCallStatus(Ringing, &reply);
if (forward && relaySipReply(reply) != 0) stopCall(StatusChangeCause::InternalError);
}
}
else {
if (getOtherId() == reply.from_tag) {
// we can relay this reply because it is from the same B leg from which
// we already relayed something
if (forward && relaySipReply(reply) != 0) stopCall(StatusChangeCause::InternalError);
}
else {
// in Ringing state but the reply comes from another B leg than
// previous 1xx reply => do not relay or process other way
DBG("1xx reply received in %s state from another B leg, ignoring\n", callStatus2str(call_status));
}
}
}
void CallLeg::b2bInitial2xx(AmSipReply& reply, bool forward)
{
if (!setOther(reply.from_tag, forward)) {
// ignore reply which comes from non-our-peer leg?
DBG("2xx reply received from unknown B leg, ignoring\n");
return;
}
DBG("setting call status to connected with leg %s\n", getOtherId().c_str());
// terminate all other legs than the connected one (determined by other_id)
terminateNotConnectedLegs();
// connect media with the other leg if RTP relay is enabled
if (!other_legs.empty())
other_legs.begin()->releaseMediaSession(); // remove reference hold by OtherLegInfo
other_legs.clear(); // no need to remember the connected leg here
onCallConnected(reply);
if (!forward) {
// we need to generate re-INVITE based on received SDP
saveSessionDescription(reply.body);
sendEstablishedReInvite();
}
else if (relaySipReply(reply) != 0) {
stopCall(StatusChangeCause::InternalError);
return;
}
updateCallStatus(Connected, &reply);
}
void CallLeg::onInitialReply(B2BSipReplyEvent *e)
{
if (e->reply.code < 200) b2bInitial1xx(e->reply, e->forward);
else if (e->reply.code < 300) b2bInitial2xx(e->reply, e->forward);
else b2bInitialErr(e->reply, e->forward);
}
void CallLeg::b2bInitialErr(AmSipReply& reply, bool forward)
{
if (getCallStatus() == Ringing && getOtherId() != reply.from_tag) {
removeOtherLeg(reply.from_tag); // we don't care about this leg any more
onBLegRefused(reply); // new B leg(s) may be added
DBG("dropping non-ok reply, it is not from current peer\n");
return;
}
DBG("clean-up after non-ok reply (reply: %d, status %s, other: %s)\n",
reply.code, callStatus2str(getCallStatus()),
getOtherId().c_str());
clearRtpReceiverRelay();
removeOtherLeg(reply.from_tag); // we don't care about this leg any more
updateCallStatus(NoReply, &reply);
onBLegRefused(reply); // possible serial fork here
set_sip_relay_only(false);
// there are other B legs for us => wait for their responses and do not
// relay current response
if (!other_legs.empty()) return;
onCallFailed(CallRefused, &reply);
if (forward) relaySipReply(reply);
// no other B legs, terminate
updateCallStatus(Disconnected, &reply);
stopCall(&reply);
}
// was for caller only
void CallLeg::onB2BReply(B2BSipReplyEvent *ev)
{
if (!ev) {
ERROR("BUG: invalid argument given\n");
return;
}
AmSipReply& reply = ev->reply;
TRACE("%s: B2B SIP reply %d/%d %s received in %s state\n",
getLocalTag().c_str(),
reply.code, reply.cseq, reply.cseq_method.c_str(),
callStatus2str(call_status));
// FIXME: testing est_invite_cseq is wrong! (checking in what direction or
// what role would be needed)
bool initial_reply = (reply.cseq_method == SIP_METH_INVITE &&
(call_status == NoReply || call_status == Ringing) &&
((reply.cseq == est_invite_cseq && ev->forward) || // related to initial INVITE at our side
(!ev->forward))); // connect not related to initial INVITE at our side
if (initial_reply) {
// handle relayed initial replies (replies to initiating INVITE at the other
// side, note that this need not to be initiating INVITE at our side)
TRACE("established CSeq: %d, forward: %s\n", est_invite_cseq, ev->forward ? "yes": "no");
onInitialReply(ev);
}
else {
// handle non-initial replies
// reply not from our peer (might be one of the discarded ones)
if (getOtherId() != ev->sender_ltag && getOtherId() != reply.from_tag) {
TRACE("ignoring reply from %s in %s state, other_id = '%s'\n",
reply.from_tag.c_str(), callStatus2str(call_status), getOtherId().c_str());
return;
}
// handle replies to other requests than the initial one
DBG("handling reply via AmB2BSession\n");
AmB2BSession::onB2BEvent(ev);
}
}
// TODO: original callee's version, update
void CallLeg::onB2BConnect(ConnectLegEvent* co_ev)
{
if (!co_ev) {
ERROR("BUG: invalid argument given\n");
return;
}
if (call_status != Disconnected) {
ERROR("BUG: ConnectLegEvent received in %s state\n", callStatus2str(call_status));
return;
}
MONITORING_LOG3(getLocalTag().c_str(),
"b2b_leg", getOtherId().c_str(),
"to", dlg->getRemoteParty().c_str(),
"ruri", dlg->getRemoteUri().c_str());
// This leg is marked as 'relay only' since the beginning because it might
// need not to know on time that it is connected and thus should relay.
//
// For example: B leg received 2xx reply, relayed it to A leg and is
// immediatelly processing in-dialog request which should be relayed, but
// A leg didn't have chance to process the relayed reply so the B leg is not
// connected to the A leg yet when handling the in-dialog request.
set_sip_relay_only(true); // we should relay everything to the other leg from now
AmMimeBody body(co_ev->body);
try {
updateLocalBody(body);
} catch (const string& s) {
relayError(SIP_METH_INVITE, co_ev->r_cseq, true, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
throw;
}
int res = dlg->sendRequest(SIP_METH_INVITE, &body,
co_ev->hdrs, SIP_FLAGS_VERBATIM);
if (res < 0) {
DBG("sending INVITE failed, relaying back error reply\n");
relayError(SIP_METH_INVITE, co_ev->r_cseq, true, res);
stopCall(StatusChangeCause::InternalError);
return;
}
updateCallStatus(NoReply);
if (co_ev->relayed_invite) {
AmSipRequest fake_req;
fake_req.method = SIP_METH_INVITE;
fake_req.cseq = co_ev->r_cseq;
relayed_req[dlg->cseq - 1] = fake_req;
est_invite_other_cseq = co_ev->r_cseq;
}
else est_invite_other_cseq = 0;
if (!co_ev->body.empty()) {
saveSessionDescription(co_ev->body);
}
// save CSeq of establising INVITE
est_invite_cseq = dlg->cseq - 1;
}
void CallLeg::onB2BReconnect(ReconnectLegEvent* ev)
{
if (!ev) {
ERROR("BUG: invalid argument given\n");
return;
}
TRACE("handling ReconnectLegEvent, other: %s, connect to %s\n",
getOtherId().c_str(), ev->session_tag.c_str());
ev->markAsProcessed();
// release old signaling and media session
clear_other();
clearRtpReceiverRelay();
relayed_req.clear();
// check if we aren't processing INVITE now (BLF ringing call pickup)
AmSipRequest *invite = dlg->getUASPendingInv();
if (invite) acceptPendingInvite(invite);
setOtherId(ev->session_tag);
if (ev->role == ReconnectLegEvent::A) a_leg = true;
else a_leg = false;
// FIXME: What about calling SBC CC modules in this case? Original CC
// interface is called from A leg only and it might happen that we were call
// leg A before.
set_sip_relay_only(true); // we should relay everything to the other leg from now
updateCallStatus(NoReply);
// use new media session if given
setRtpRelayMode(ev->rtp_mode);
if (ev->media) {
setMediaSession(ev->media);
getMediaSession()->changeSession(a_leg, this);
}
MONITORING_LOG3(getLocalTag().c_str(),
"b2b_leg", getOtherId().c_str(),
"to", dlg->getRemoteParty().c_str(),
"ruri", dlg->getRemoteUri().c_str());
updateSession(new Reinvite(ev->hdrs, ev->body,
/* establishing = */ true, ev->relayed_invite, ev->r_cseq));
}
void CallLeg::onB2BReplace(ReplaceLegEvent *e)
{
if (!e) {
ERROR("BUG: invalid argument given\n");
return;
}
e->markAsProcessed();
ReconnectLegEvent *reconnect = e->getReconnectEvent();
if (!reconnect) {
ERROR("BUG: invalid ReconnectLegEvent\n");
return;
}
TRACE("handling ReplaceLegEvent, other: %s, connect to %s\n",
getOtherId().c_str(), reconnect->session_tag.c_str());
string id(getOtherId());
if (id.empty()) {
// try it with the first B leg?
if (other_legs.empty()) {
ERROR("BUG: there is no B leg to connect our replacement to\n");
return;
}
id = other_legs[0].id;
}
// send session ID of the other leg to the originator
AmSessionContainer::instance()->postEvent(reconnect->session_tag, new ReplaceInProgressEvent(id));
// send the ReconnectLegEvent to the other leg
AmSessionContainer::instance()->postEvent(id, reconnect);
// remove the B leg from our B leg list
removeOtherLeg(id);
// commit suicide if our last B leg is stolen
if (other_legs.empty() && getOtherId().empty()) stopCall(StatusChangeCause::Other /* FIXME? */);
}
void CallLeg::onB2BReplaceInProgress(ReplaceInProgressEvent *e)
{
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
if (i->id.empty()) {
// replace the temporary (invalid) session with the correct one
i->id = e->dst_session;
return;
}
}
}
void CallLeg::disconnect(bool hold_remote, bool preserve_media_session)
{
TRACE("disconnecting call leg %s from the other\n", getLocalTag().c_str());
switch (call_status) {
case Disconnecting:
case Disconnected:
DBG("trying to disconnect already disconnected (or disconnecting) call leg\n");
return;
case NoReply:
case Ringing:
WARN("trying to disconnect in not connected state, terminating not connected legs in advance (was it intended?)\n");
terminateNotConnectedLegs();
// do not break, continue with following state handling!
case Connected:
if (!preserve_media_session) {
// we can't stay connected (at media level) with the other leg
clearRtpReceiverRelay();
}
break; // this is OK
}
// create new media session for us if needed
if (getRtpRelayMode() != RTP_Direct && !preserve_media_session)
setMediaSession(new AmB2BMedia(a_leg ? this: NULL, a_leg ? NULL : this));
clear_other();
set_sip_relay_only(false); // we can't relay once disconnected
est_invite_cseq = 0; // attempt to invalidate though 0 is valid value
relayed_req.clear(); // do not forward anything back any more
if (!hold_remote || isOnHold()) updateCallStatus(Disconnected);
else {
updateCallStatus(Disconnecting);
putOnHold();
}
}
static void sdp2body(const AmSdp &sdp, AmMimeBody &body)
{
string body_str;
sdp.print(body_str);
AmMimeBody *s = body.hasContentType(SIP_APPLICATION_SDP);
if (s) s->parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
else body.parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
}
int CallLeg::putOnHoldImpl()
{
if (on_hold) return -1; // no request went out
TRACE("putting remote on hold\n");
hold = HoldRequested;
holdRequested();
AmSdp sdp;
createHoldRequest(sdp);
updateLocalSdp(sdp);
AmMimeBody body;
sdp2body(sdp, body);
if (dlg->reinvite("", &body, SIP_FLAGS_VERBATIM) != 0) {
ERROR("re-INVITE failed\n");
offerRejected();
return -1;
}
return dlg->cseq - 1;
}
int CallLeg::resumeHeldImpl()
{
if (!on_hold) return -1;
try {
TRACE("resume held remote\n");
hold = ResumeRequested;
resumeRequested();
AmSdp sdp;
createResumeRequest(sdp);
if (sdp.media.empty()) {
ERROR("invalid un-hold SDP, can't unhold\n");
offerRejected();
return -1;
}
updateLocalSdp(sdp);
AmMimeBody body(established_body);
sdp2body(sdp, body);
if (dlg->reinvite("", &body, SIP_FLAGS_VERBATIM) != 0) {
ERROR("re-INVITE failed\n");
offerRejected();
return -1;
}
return dlg->cseq - 1;
}
catch (...) {
offerRejected();
return -1;
}
}
void CallLeg::holdAccepted()
{
DBG("hold accepted on %c leg\n", a_leg?'B':'A');
if (call_status == Disconnecting) updateCallStatus(Disconnected);
on_hold = true;
AmB2BMedia *ms = getMediaSession();
if (ms) {
DBG("holdAccepted - mute %c leg\n", a_leg?'B':'A');
ms->mute(!a_leg); // mute the stream in other (!) leg
}
}
void CallLeg::holdRejected()
{
if (call_status == Disconnecting) updateCallStatus(Disconnected);
}
void CallLeg::resumeAccepted()
{
on_hold = false;
AmB2BMedia *ms = getMediaSession();
if (ms) ms->unmute(!a_leg); // unmute the stream in other (!) leg
DBG("%s: resuming held, unmuting media session %p(%s)\n", getLocalTag().c_str(), ms, !a_leg ? "A" : "B");
}
// was for caller only
void CallLeg::onInvite(const AmSipRequest& req)
{
// do not call AmB2BSession::onInvite(req); we changed the behavior
// this method is not called for re-INVITEs because once connected we are in
// sip_relay_only mode and the re-INVITEs are relayed instead of processing
// (see AmB2BSession::onSipRequest)
if (call_status == Disconnected) { // for initial INVITE only
est_invite_cseq = req.cseq; // remember initial CSeq
// initialize RTP relay
// relayed INVITE - we need to add the original INVITE to
// list of received (relayed) requests
recvd_req.insert(std::make_pair(req.cseq, req));
}
}
void CallLeg::onSipRequest(const AmSipRequest& req)
{
TRACE("%s: SIP request %d %s received in %s state\n",
getLocalTag().c_str(),
req.cseq, req.method.c_str(), callStatus2str(call_status));
// we need to handle cases if there is no other leg (for example call parking)
// Note that setting sip_relay_only to false in this case doesn't solve the
// problem because AmB2BSession always tries to relay the request into the
// other leg.
if ((getCallStatus() == Disconnected || getCallStatus() == Disconnecting)
&& getOtherId().empty())
{
TRACE("handling request %s in disconnected state", req.method.c_str());
// this is not correct but what is?
// handle reINVITEs within B2B call with no other leg
if (req.method == SIP_METH_INVITE && dlg->getStatus() == AmBasicSipDialog::Connected) {
try {
dlg->reply(req, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
}
catch(...) {
ERROR("exception when handling INVITE in disconnected state");
dlg->reply(req, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
// stop the call?
}
}
else AmSession::onSipRequest(req);
if (req.method == SIP_METH_BYE) {
stopCall(&req); // is this needed?
}
}
else {
if(getCallStatus() == Disconnected &&
req.method == SIP_METH_BYE) {
// seems that we have already sent/received a BYE
// -> we'd better terminate this ASAP
// to avoid other confusions...
dlg->reply(req,200,"OK");
}
else
AmB2BSession::onSipRequest(req);
}
}
void CallLeg::onSipReply(const AmSipRequest& req, const AmSipReply& reply, AmSipDialog::Status old_dlg_status)
{
TransMap::iterator t = relayed_req.find(reply.cseq);
bool relayed_request = (t != relayed_req.end());
TRACE("%s: SIP reply %d/%d %s (%s) received in %s state\n",
getLocalTag().c_str(),
reply.code, reply.cseq, reply.cseq_method.c_str(),
(relayed_request ? "to relayed request" : "to locally generated request"),
callStatus2str(call_status));
#if 0
if ((oa.hold != OA::PreserveHoldStatus) && (!relayed_request)) {
INFO("locally generated hold/resume request replied, not handling by B2B\n");
// local hold/resume request replied, we don't want to relay this reply to the other leg!
// => do the necessary stuff here (copy & paste from AmB2BSession::onSipReply)
if (reply.code < 300) {
const AmMimeBody *sdp_part = reply.body.hasContentType(SIP_APPLICATION_SDP);
if (sdp_part) {
AmSdp sdp;
if (sdp.parse((const char *)sdp_part->getPayload()) == 0) updateRemoteSdp(sdp);
}
}
else if (reply.code >= 300) offerRejected();
AmSession::onSipReply(req, reply, old_dlg_status);
return;
}
#endif
if (reply.code >= 300 && reply.cseq_method == SIP_METH_INVITE) offerRejected();
// handle final replies of session updates in progress
if (!pending_updates.empty() && reply.code >= 200 && pending_updates.front()->hasCSeq(reply.cseq)) {
if (reply.code == 491) {
pending_updates.front()->reset();
double t = get491RetryTime();
pending_updates_timer.start(getLocalTag(), t);
TRACE("planning to retry update operation in %gs", t);
}
else {
// TODO: 503, ...
delete pending_updates.front();
pending_updates.pop_front();
}
}
AmB2BSession::onSipReply(req, reply, old_dlg_status);
// update internal state and call related callbacks based on received reply
// (i.e. B leg in case of initial INVITE)
if (reply.cseq == est_invite_cseq && reply.cseq_method == SIP_METH_INVITE &&
(call_status == NoReply || call_status == Ringing)) {
// reply to the initial request
if ((reply.code > 100) && (reply.code < 200)) {
if (((call_status == NoReply)) && (!reply.to_tag.empty()))
updateCallStatus(Ringing, &reply);
}
else if ((reply.code >= 200) && (reply.code < 300)) {
onCallConnected(reply);
updateCallStatus(Connected, &reply);
}
else if (reply.code >= 300) {
updateCallStatus(Disconnected, &reply);
terminateLeg(); // commit suicide (don't let the master to kill us)
}
}
// update call registry (unfortunately has to be done always -
// not possible to determine if learned in this reply (?))
if (!dlg->getRemoteTag().empty() && reply.code >= 200 && req.method == SIP_METH_INVITE) {
SBCCallRegistry::updateCall(getOtherId(), dlg->getRemoteTag());
}
}
// was for caller only
void CallLeg::onInvite2xx(const AmSipReply& reply)
{
// We don't want to remember reply.cseq as est_invite_cseq, do we? It was in
// AmB2BCallerSession but we already have initial INVITE cseq remembered and
// we don't need to change it to last reINVITE one, right? Otherwise we should
// remember UPDATE cseq as well because SDP may change by it as well (used
// when handling B2BSipReply in AmB2BSession to check if reINVITE should be
// sent).
//
// est_invite_cseq = reply.cseq;
// we don't want to handle the 2xx using AmSession so the following may be
// unwanted for us:
//
AmB2BSession::onInvite2xx(reply);
}
void CallLeg::onCancel(const AmSipRequest& req)
{
// initial INVITE handling
if ((call_status == Ringing) || (call_status == NoReply)) {
if (a_leg) {
// terminate whole B2B call if the caller receives CANCEL
onCallFailed(CallCanceled, NULL);
updateCallStatus(Disconnected, StatusChangeCause::Canceled);
stopCall(StatusChangeCause::Canceled);
}
// else { } ... ignore for B leg
}
}
void CallLeg::terminateLeg()
{
AmB2BSession::terminateLeg();
}
// was for caller only
void CallLeg::onRemoteDisappeared(const AmSipReply& reply)
{
if (call_status == Connected) {
// only in case we are really connected
// (called on timeout or 481 from the remote)
DBG("remote unreachable, ending B2BUA call\n");
// FIXME: shouldn't be cleared in AmB2BSession as well?
clearRtpReceiverRelay();
AmB2BSession::onRemoteDisappeared(reply); // terminates the other leg
updateCallStatus(Disconnected, &reply);
}
}
// was for caller only
void CallLeg::onBye(const AmSipRequest& req)
{
terminateNotConnectedLegs();
updateCallStatus(Disconnected, &req);
clearRtpReceiverRelay(); // FIXME: shouldn't be cleared in AmB2BSession as well?
AmB2BSession::onBye(req);
}
void CallLeg::onOtherBye(const AmSipRequest& req)
{
updateCallStatus(Disconnected, &req);
AmB2BSession::onOtherBye(req);
}
void CallLeg::onNoAck(unsigned int cseq)
{
updateCallStatus(Disconnected, StatusChangeCause::NoAck);
AmB2BSession::onNoAck(cseq);
}
void CallLeg::onNoPrack(const AmSipRequest &req, const AmSipReply &rpl)
{
updateCallStatus(Disconnected, StatusChangeCause::NoPrack);
AmB2BSession::onNoPrack(req, rpl);
}
void CallLeg::onRtpTimeout()
{
updateCallStatus(Disconnected, StatusChangeCause::RtpTimeout);
AmB2BSession::onRtpTimeout();
}
void CallLeg::onSessionTimeout()
{
updateCallStatus(Disconnected, StatusChangeCause::SessionTimeout);
AmB2BSession::onSessionTimeout();
}
// AmMediaSession interface from AmMediaProcessor
int CallLeg::readStreams(unsigned long long ts, unsigned char *buffer) {
// skip RTP processing if in Relay mode
// (but we want to process DTMF thus we may be in media processor)
if (getRtpRelayMode()==RTP_Relay)
return 0;
return AmB2BSession::readStreams(ts, buffer);
}
int CallLeg::writeStreams(unsigned long long ts, unsigned char *buffer) {
// skip RTP processing if in Relay mode
// (but we want to process DTMF thus we may be in media processor)
if (getRtpRelayMode()==RTP_Relay)
return 0;
return AmB2BSession::writeStreams(ts, buffer);
}
void CallLeg::addNewCallee(CallLeg *callee, ConnectLegEvent *e,
AmB2BSession::RTPRelayMode mode)
{
OtherLegInfo b;
b.id = callee->getLocalTag();
callee->setRtpRelayMode(mode);
if (mode != RTP_Direct) {
// do not initialise the media session with A leg to avoid unnecessary A leg
// RTP stream creation in every B leg's media session
if (a_leg) b.media_session = new AmB2BMedia(NULL, callee);
else b.media_session = new AmB2BMedia(callee, NULL);
b.media_session->addReference(); // new reference for me
callee->setMediaSession(b.media_session);
}
else b.media_session = NULL;
other_legs.push_back(b);
if (AmConfig::LogSessions) {
TRACE("Starting B2B callee session %s\n",
callee->getLocalTag().c_str()/*, invite_req.cmd.c_str()*/);
}
AmSipDialog* callee_dlg = callee->dlg;
MONITORING_LOG4(b.id.c_str(),
"dir", "out",
"from", callee_dlg->getLocalParty().c_str(),
"to", callee_dlg->getRemoteParty().c_str(),
"ruri", callee_dlg->getRemoteUri().c_str());
callee->start();
AmSessionContainer* sess_cont = AmSessionContainer::instance();
sess_cont->addSession(b.id, callee);
// generate connect event to the newly added leg
// Warning: correct callee's role must be already set (in constructor or so)
TRACE("relaying connect leg event to the new leg\n");
// other stuff than relayed INVITE should be set directly when creating callee
// (remote_uri, remote_party is not propagated and thus B2BConnectEvent is not
// used because it would just overwrite already set things. Note that in many
// classes derived from AmB2BCaller[Callee]Session was a lot of things set
// explicitly)
AmSessionContainer::instance()->postEvent(b.id, e);
if (call_status == Disconnected) updateCallStatus(NoReply);
}
void CallLeg::setCallStatus(CallStatus new_status)
{
call_status = new_status;
}
const char* CallLeg::getCallStatusStr() {
switch(getCallStatus()) {
case Disconnected : return "Disconnected";
case NoReply : return "NoReply";
case Ringing : return "Ringing";
case Connected : return "Connected";
case Disconnecting : return "Disconnecting";
default: return "Unknown";
};
}
void CallLeg::updateCallStatus(CallStatus new_status, const StatusChangeCause &cause)
{
if (new_status == Connected)
TRACE("%s leg %s changing status from %s to %s with %s\n",
a_leg ? "A" : "B",
getLocalTag().c_str(),
callStatus2str(call_status),
callStatus2str(new_status),
getOtherId().c_str());
else
TRACE("%s leg %s changing status from %s to %s\n",
a_leg ? "A" : "B",
getLocalTag().c_str(),
callStatus2str(call_status),
callStatus2str(new_status));
setCallStatus(new_status);
onCallStatusChange(cause);
}
void CallLeg::addExistingCallee(const string &session_tag, ReconnectLegEvent *ev)
{
// add existing session as our B leg
OtherLegInfo b;
b.id = session_tag;
if (rtp_relay_mode != RTP_Direct) {
// do not initialise the media session with A leg to avoid unnecessary A leg
// RTP stream creation in every B leg's media session
b.media_session = new AmB2BMedia(NULL, NULL);
b.media_session->addReference(); // new reference for me
}
else b.media_session = NULL;
// generate connect event to the newly added leg
TRACE("relaying re-connect leg event to the B leg\n");
ev->setMedia(b.media_session, rtp_relay_mode);
// TODO: what about the RTP relay and other settings? send them as well?
if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
// session doesn't exist - can't connect
INFO("the B leg to connect to (%s) doesn't exist\n", session_tag.c_str());
if (b.media_session) {
b.media_session->releaseReference();
b.media_session = NULL; // ptr may not be valid any more
}
return;
}
other_legs.push_back(b);
if (call_status == Disconnected) updateCallStatus(NoReply);
}
void CallLeg::addCallee(const string &session_tag, const AmSipRequest &relayed_invite)
{
addExistingCallee(session_tag, new ReconnectLegEvent(getLocalTag(), relayed_invite));
}
void CallLeg::addCallee(CallLeg *callee, const string &hdrs)
{
if (!non_hold_sdp.media.empty()) {
// use non-hold SDP if possible
AmMimeBody body(established_body);
sdp2body(non_hold_sdp, body);
addNewCallee(callee, new ConnectLegEvent(hdrs, body));
}
else addNewCallee(callee, new ConnectLegEvent(hdrs, established_body));
}
/*void CallLeg::addCallee(CallLeg *callee, const string &hdrs, AmB2BSession::RTPRelayMode mode)
{
addNewCallee(callee, new ConnectLegEvent(hdrs, established_body), mode);
}*/
void CallLeg::replaceExistingLeg(const string &session_tag, const AmSipRequest &relayed_invite)
{
// add existing session as our B leg
OtherLegInfo b;
b.id.clear(); // this is an invalid local tag (temporarily)
if (rtp_relay_mode != RTP_Direct) {
// let the other leg to set its part, we will set our once connected
b.media_session = new AmB2BMedia(NULL, NULL);
b.media_session->addReference(); // new reference for me
}
else b.media_session = NULL;
ReplaceLegEvent *ev = new ReplaceLegEvent(getLocalTag(), relayed_invite, b.media_session, rtp_relay_mode);
// TODO: what about the RTP relay and other settings? send them as well?
if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
// session doesn't exist - can't connect
INFO("the call leg to be replaced (%s) doesn't exist\n", session_tag.c_str());
if (b.media_session) {
b.media_session->releaseReference();
b.media_session = NULL;
}
return;
}
other_legs.push_back(b);
if (call_status == Disconnected) updateCallStatus(NoReply); // we are something like connected to another leg
}
void CallLeg::replaceExistingLeg(const string &session_tag, const string &hdrs)
{
// add existing session as our B leg
OtherLegInfo b;
b.id.clear(); // this is an invalid local tag (temporarily)
if (rtp_relay_mode != RTP_Direct) {
// let the other leg to set its part, we will set our once connected
b.media_session = new AmB2BMedia(NULL, NULL);
b.media_session->addReference(); // new reference for me
}
else b.media_session = NULL;
ReconnectLegEvent *rev = new ReconnectLegEvent(a_leg ? ReconnectLegEvent::B : ReconnectLegEvent::A, getLocalTag(), hdrs, established_body);
rev->setMedia(b.media_session, rtp_relay_mode);
ReplaceLegEvent *ev = new ReplaceLegEvent(getLocalTag(), rev);
// TODO: what about the RTP relay and other settings? send them as well?
if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
// session doesn't exist - can't connect
INFO("the call leg to be replaced (%s) doesn't exist\n", session_tag.c_str());
if (b.media_session) {
b.media_session->releaseReference();
b.media_session = NULL;
}
return;
}
other_legs.push_back(b);
if (call_status == Disconnected) updateCallStatus(NoReply); // we are something like connected to another leg
}
void CallLeg::clear_other()
{
removeOtherLeg(getOtherId());
AmB2BSession::clear_other();
}
void CallLeg::stopCall(const StatusChangeCause &cause) {
if (getCallStatus() != Disconnected) updateCallStatus(Disconnected, cause);
terminateNotConnectedLegs();
terminateOtherLeg();
terminateLeg();
}
void CallLeg::changeRtpMode(RTPRelayMode new_mode)
{
if (new_mode == rtp_relay_mode) return; // requested mode is set already
// we don't need to send reINVITE from here, expecting caller knows what is he
// doing (it is probably processing or generating its own reINVITE)
// Switch from RTP_Direct to RTP_Relay is safe (no audio loss), the other can
// be lossy because already existing media object would be destroyed.
// FIXME: use AmB2BMedia in all RTP relay modes to avoid these problems?
clearRtpReceiverRelay();
setRtpRelayMode(new_mode);
switch (getCallStatus()) {
case CallLeg::Connected:
case CallLeg::Disconnecting:
case CallLeg::Disconnected:
if (new_mode == RTP_Relay || new_mode == RTP_Transcoding)
setMediaSession(new AmB2BMedia(a_leg ? this: NULL, a_leg ? NULL : this));
if (!getOtherId().empty())
relayEvent(new ChangeRtpModeEvent(new_mode, getMediaSession()));
break;
case CallLeg::NoReply:
case CallLeg::Ringing:
if (other_legs.empty()) {
// we will receive our media session from the peer later on
// WARNING: this means that getMediaSession called before we receive one
// will give unusable instance (NULL for now)
if (!getOtherId().empty())
relayEvent(new ChangeRtpModeEvent(new_mode, getMediaSession()));
}
else {
// we have to release or generate new media sessions for all our B legs
changeOtherLegsRtpMode(new_mode);
}
break;
}
switch (dlg->getOAState()) {
case AmOfferAnswer::OA_Completed:
case AmOfferAnswer::OA_None:
// must be followed by OA exchange because we can't updateLocalSdp
// (reINVITE would be needed)
break;
case AmOfferAnswer::OA_OfferSent:
TRACE("changing RTP mode after offer was sent: reINVITE needed\n");
// TODO: plan a reINVITE
ERROR("not implemented\n");
break;
case AmOfferAnswer::OA_OfferRecved:
TRACE("changing RTP mode after offer was received\n");
break;
case AmOfferAnswer::__max_OA: break; // grrrr
}
}
void CallLeg::changeRtpMode(RTPRelayMode new_mode, AmB2BMedia *new_media)
{
// we need to process regardless old RTP mode (at least new B2B media session
// has to be used)
bool mode_changed = (getRtpRelayMode() != new_mode);
clearRtpReceiverRelay();
setRtpRelayMode(new_mode);
switch (getCallStatus()) {
case CallLeg::Connected:
case CallLeg::Disconnecting:
case CallLeg::Disconnected:
setMediaSession(new_media);
break;
case CallLeg::NoReply:
case CallLeg::Ringing:
if (other_legs.empty()) {
// we are not the "A leg", we can use supplied media session
setMediaSession(new_media);
}
else {
// we have to release or generate new media sessions for all our B legs
// (ignoring supplied media)
// WARNING: we will use the same RTP relay mode for all peer legs!
if (mode_changed) changeOtherLegsRtpMode(new_mode);
}
break;
}
AmB2BMedia *m = getMediaSession();
if (m) m->changeSession(a_leg, this);
switch (dlg->getOAState()) {
case AmOfferAnswer::OA_Completed:
case AmOfferAnswer::OA_None:
// must be followed by OA exchange because we can't updateLocalSdp
// (reINVITE would be needed)
break;
case AmOfferAnswer::OA_OfferSent:
TRACE("changing RTP mode/media session after offer was sent: reINVITE needed\n");
// TODO: plan a reINVITE
ERROR("%s: not implemented\n", getLocalTag().c_str());
break;
case AmOfferAnswer::OA_OfferRecved:
TRACE("changing RTP mode/media session after offer was received\n");
break;
case AmOfferAnswer::__max_OA: break; // grrrr
}
}
void CallLeg::changeOtherLegsRtpMode(RTPRelayMode new_mode)
{
// change RTP relay mode and media session for all in other_legs
const string &other = getOtherId();
for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
i->releaseMediaSession();
if (new_mode != RTP_Direct) {
i->media_session = new AmB2BMedia(NULL, NULL);
i->media_session->addReference(); // new reference for storage
if (other == i->id && i->media_session) {
// if connected already with one of the legs we have to use the same
// media session for us
setMediaSession(i->media_session);
if (i->media_session) i->media_session->changeSession(a_leg, this);
}
}
AmSessionContainer::instance()->postEvent(i->id, new ChangeRtpModeEvent(new_mode, i->media_session));
}
}
void CallLeg::acceptPendingInvite(AmSipRequest *invite)
{
// reply the INVITE with fake 200 reply
AmMimeBody *sdp = invite->body.hasContentType(SIP_APPLICATION_SDP);
AmSdp s;
if (!sdp || s.parse((const char*)sdp->getPayload())) {
// no offer in the INVITE (or can't be parsed), we have to append fake offer
// into the reply
s.version = 0;
s.origin.user = "sems";
s.sessionName = "sems";
s.conn.network = NT_IN;
s.conn.addrType = AT_V4;
s.conn.address = "0.0.0.0";
s.media.push_back(SdpMedia());
SdpMedia &m = s.media.back();
m.type = MT_AUDIO;
m.transport = TP_RTPAVP;
m.send = false;
m.recv = false;
m.payloads.push_back(SdpPayload(0));
}
if (!s.conn.address.empty()) s.conn.address = "0.0.0.0";
for (vector<SdpMedia>::iterator i = s.media.begin(); i != s.media.end(); ++i) {
//i->port = 0;
if (!i->conn.address.empty()) i->conn.address = "0.0.0.0";
}
AmMimeBody body;
string body_str;
s.print(body_str);
body.parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
try {
updateLocalBody(body);
} catch (...) { /* throw ? */ }
TRACE("replying pending INVITE with body: %s\n", body_str.c_str());
dlg->reply(*invite, 200, "OK", &body);
if (getCallStatus() != Connected) updateCallStatus(Connected);
}
int CallLeg::reinvite(const string &hdrs, const AmMimeBody &body, bool relayed, unsigned r_cseq, bool establishing)
{
int res;
try {
AmMimeBody r_body(body);
updateLocalBody(r_body);
res = dlg->sendRequest(SIP_METH_INVITE, &r_body, hdrs, SIP_FLAGS_VERBATIM);
} catch (const string& s) { res = -500; }
if (res < 0) {
if (relayed) {
DBG("sending re-INVITE failed, relaying back error reply\n");
relayError(SIP_METH_INVITE, r_cseq, true, res);
}
DBG("sending re-INVITE failed, terminating the call\n");
stopCall(StatusChangeCause::InternalError);
return -1;
}
if (relayed) {
AmSipRequest fake_req;
fake_req.method = SIP_METH_INVITE;
fake_req.cseq = r_cseq;
relayed_req[dlg->cseq - 1] = fake_req;
est_invite_other_cseq = r_cseq;
}
else est_invite_other_cseq = 0;
saveSessionDescription(body);
if (establishing) {
// save CSeq of establishing INVITE
est_invite_cseq = dlg->cseq - 1;
}
return dlg->cseq - 1;
}
void CallLeg::adjustOffer(AmSdp &sdp)
{
if (hold != PreserveHoldStatus) {
DBG("local hold/unhold request");
// locally generated hold/unhold requests that already contain correct
// hold/resume bodies and need not to be altered via createHoldRequest
// hold/resumeRequested is already called
}
else {
// handling B2B SDP, check for hold/unhold
HoldMethod hm;
// if hold request, transform to requested kind of hold and remember that hold
// was requested with this offer
if (isHoldRequest(sdp, hm)) {
DBG("B2b hold request");
holdRequested();
alterHoldRequest(sdp);
hold = HoldRequested;
}
else {
if (on_hold) {
DBG("B2b resume request");
resumeRequested();
alterResumeRequest(sdp);
hold = ResumeRequested;
}
}
}
}
void CallLeg::updateLocalSdp(AmSdp &sdp)
{
TRACE("%s: updateLocalSdp (OA: %d)\n", getLocalTag().c_str(), dlg->getOAState());
// handle the body based on current offer-answer status
// (possibly update the body before sending to remote)
// FIXME: repeated SDP (183, 200) will cause false match in OA_Completed
// (need not to be expected with re-INVITEs asking for hold)
if (dlg->getOAState() == AmOfferAnswer::OA_None ||
dlg->getOAState() == AmOfferAnswer::OA_Completed)
{
// handling offer
adjustOffer(sdp);
}
if (hold == PreserveHoldStatus && !on_hold) {
// store non-hold SDP to be able to resumeHeld
non_hold_sdp = sdp;
}
AmB2BSession::updateLocalSdp(sdp);
}
void CallLeg::offerRejected()
{
TRACE("%s: offer rejected! (hold status: %d)", getLocalTag().c_str(), hold);
switch (hold) {
case HoldRequested: holdRejected(); break;
case ResumeRequested: resumeRejected(); break;
case PreserveHoldStatus: break;
}
hold = PreserveHoldStatus;
}
void CallLeg::createResumeRequest(AmSdp &sdp)
{
// use stored non-hold SDP
// Note: this SDP doesn't need to be correct, but established_body need not to
// be good enough for unholding (might be held already with zero conncetions)
if (!non_hold_sdp.media.empty()) sdp = non_hold_sdp;
else {
// no stored non-hold SDP
ERROR("no stored non-hold SDP, but local resume requested\n");
// TODO: try to use established_body here and mark properly
// if no established body exist
throw string("not implemented");
}
// do not touch the sdp otherwise (use directly B2B SDP)
}
void CallLeg::debug()
{
DBG("call leg: %s", getLocalTag().c_str());
DBG("\tother: %s\n", getOtherId().c_str());
DBG("\tstatus: %s\n", callStatus2str(getCallStatus()));
DBG("\tRTP relay mode: %d\n", rtp_relay_mode);
DBG("\ton hold: %s\n", on_hold ? "yes" : "no");
DBG("\toffer/answer status: %d, hold: %d\n", dlg->getOAState(), hold);
AmB2BMedia *ms = getMediaSession();
if (ms) ms->debug();
}
int CallLeg::onSdpCompleted(const AmSdp& offer, const AmSdp& answer)
{
TRACE("%s: oaCompleted\n", getLocalTag().c_str());
switch (hold) {
case HoldRequested: holdAccepted(); break;
case ResumeRequested: resumeAccepted(); break;
case PreserveHoldStatus: break;
}
hold = PreserveHoldStatus;
return AmB2BSession::onSdpCompleted(offer, answer);
}
void CallLeg::applyPendingUpdate()
{
TRACE("going to apply pending updates");
if (pending_updates.empty()) return;
if (!canUpdateSession()) {
TRACE("can't apply pending updates now");
return;
}
TRACE("applying pending updates");
do {
SessionUpdate *u = pending_updates.front();
u->apply(this);
if (u->hasCSeq()) {
// SIP transaction started, wait for finishing it
break;
}
else {
// the update operation hasn't started a SIP transaction so it can be
// understood as finished
pending_updates.pop_front();
delete u;
}
} while (!pending_updates.empty());
}
void CallLeg::onTransFinished()
{
TRACE("UAC/UAS transaction finished");
AmB2BSession::onTransFinished();
if (pending_updates.empty() || !canUpdateSession()) return; // there is nothing we can do now
if (pending_updates_timer.started()) {
TRACE("UAC/UAS transaction finished, but waiting for planned updates");
return; // it is planned to apply the updates later on
}
TRACE("UAC/UAS transaction finished, try to apply pending updates");
AmSessionContainer::instance()->postEvent(getLocalTag(), new ApplyPendingUpdatesEvent());
}
void CallLeg::updateSession(SessionUpdate *u)
{
if (!canUpdateSession() || !pending_updates.empty()) {
TRACE("planning session update for later");
pending_updates.push_back(u);
}
else {
u->apply(this);
if (u->hasCSeq()) pending_updates.push_back(u); // store for failover
else delete u; // finished
}
}
void CallLeg::putOnHold()
{
updateSession(new PutOnHold());
}
void CallLeg::resumeHeld()
{
updateSession(new ResumeHeld());
}