mirror of https://github.com/sipwise/sems.git
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.
1760 lines
54 KiB
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());
|
|
}
|