Code owners
Assign users and groups as approvers for specific file changes. Learn more.
sipcall.cpp 55.35 KiB
/*
* Copyright (C) 2004-2021 Savoir-faire Linux Inc.
*
* Author: Emmanuel Milou <emmanuel.milou@savoirfairelinux.com>
* Author: Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
* Author: Yan Morin <yan.morin@savoirfairelinux.com>
* Author: Laurielle Lea <laurielle.lea@savoirfairelinux.com>
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
* Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "call_factory.h"
#include "sipcall.h"
#include "sipaccount.h" // for SIPAccount::ACCOUNT_TYPE
#include "sipaccountbase.h"
#include "sipvoiplink.h"
#include "logger.h" // for _debug
#include "sdp.h"
#include "manager.h"
#include "string_utils.h"
#include "upnp/upnp_control.h"
#include "sip_utils.h"
#include "audio/audio_rtp_session.h"
#include "system_codec_container.h"
#include "im/instant_messaging.h"
#include "dring/call_const.h"
#include "dring/media_const.h"
#include "client/ring_signal.h"
#include "ice_transport.h"
#ifdef ENABLE_PLUGIN
// Plugin manager
#include "plugin/jamipluginmanager.h"
#endif
#ifdef ENABLE_VIDEO
#include "client/videomanager.h"
#include "video/video_rtp_session.h"
#include "dring/videomanager_interface.h"
#include <chrono>
#include <libavutil/display.h>
#endif
#include "errno.h"
#include <opendht/crypto.h>
#include <opendht/thread_pool.h>
namespace jami {
using sip_utils::CONST_PJ_STR;
#ifdef ENABLE_VIDEO
static DeviceParams
getVideoSettings()
{
const auto& videomon = jami::getVideoDeviceMonitor();
return videomon.getDeviceParams(videomon.getDefaultDevice());
}
#endif
static constexpr std::chrono::seconds DEFAULT_ICE_INIT_TIMEOUT {35}; // seconds
static constexpr std::chrono::seconds DEFAULT_ICE_NEGO_TIMEOUT {60}; // seconds
static constexpr std::chrono::milliseconds MS_BETWEEN_2_KEYFRAME_REQUEST {1000};
// SDP media Ids
static constexpr int SDP_AUDIO_MEDIA_ID {0};
static constexpr int SDP_VIDEO_MEDIA_ID {1};
// ICE components Id used on SIP
static constexpr int ICE_AUDIO_RTP_COMPID {0};
static constexpr int ICE_AUDIO_RTCP_COMPID {1};
static constexpr int ICE_VIDEO_RTP_COMPID {2};
static constexpr int ICE_VIDEO_RTCP_COMPID {3};
const char* const SIPCall::LINK_TYPE = SIPAccount::ACCOUNT_TYPE;
SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
const std::string& id,
Call::CallType type,
const std::map<std::string, std::string>& details)
: Call(account, id, type, details)
, avformatrtp_(new AudioRtpSession(id))
#ifdef ENABLE_VIDEO
// The ID is used to associate video streams to calls
, videortp_(new video::VideoRtpSession(id, getVideoSettings()))
, mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
#endif
, sdp_(new Sdp(id))
{
if (account->getUPnPActive())
upnp_.reset(new upnp::Controller());
setCallMediaLocal();
}
SIPCall::~SIPCall()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
{
std::lock_guard<std::mutex> lk(transportMtx_);
if (tmpMediaTransport_)
dht::ThreadPool::io().run([ice = std::make_shared<decltype(tmpMediaTransport_)>(
std::move(tmpMediaTransport_))] {});
}
setTransport({});
inv.reset(); // prevents callback usage
}
std::shared_ptr<SIPAccountBase>
SIPCall::getSIPAccount() const
{
return std::static_pointer_cast<SIPAccountBase>(getAccount().lock());
}
#ifdef ENABLE_PLUGIN
void
SIPCall::createCallAVStreams()
{
/**
* Map: maps the AudioFrame to an AVFrame
**/
auto audioMap = [](const std::shared_ptr<jami::MediaFrame> m) -> AVFrame* {
return std::static_pointer_cast<AudioFrame>(m)->pointer();
};
// Preview
if (auto& localAudio = avformatrtp_->getAudioLocal()) {
auto previewSubject = std::make_shared<MediaStreamSubject>(audioMap);
StreamData microStreamData {getCallId(), false, StreamType::audio, getPeerNumber()};
createCallAVStream(microStreamData, *localAudio, previewSubject);
}
// Receive
if (auto& audioReceive = avformatrtp_->getAudioReceive()) {
auto receiveSubject = std::make_shared<MediaStreamSubject>(audioMap);
StreamData phoneStreamData {getCallId(), true, StreamType::audio, getPeerNumber()};
createCallAVStream(phoneStreamData, (AVMediaStream&) *audioReceive, receiveSubject);
}
#ifdef ENABLE_VIDEO
if (hasVideo()) {
/**
* Map: maps the VideoFrame to an AVFrame
**/
auto videoMap = [](const std::shared_ptr<jami::MediaFrame> m) -> AVFrame* {
return std::static_pointer_cast<VideoFrame>(m)->pointer();
};
// Preview
if (auto& videoPreview = videortp_->getVideoLocal()) {
auto previewSubject = std::make_shared<MediaStreamSubject>(videoMap);
StreamData previewStreamData {getCallId(), false, StreamType::video, getPeerNumber()};
createCallAVStream(previewStreamData, *videoPreview, previewSubject);
}
// Receive
if (auto& videoReceive = videortp_->getVideoReceive()) {
auto receiveSubject = std::make_shared<MediaStreamSubject>(videoMap);
StreamData receiveStreamData {getCallId(), true, StreamType::video, getPeerNumber()};
createCallAVStream(receiveStreamData, *videoReceive, receiveSubject);
}
}
#endif
}
void
SIPCall::createCallAVStream(const StreamData& StreamData,
AVMediaStream& streamSource,
const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject)
{
const std::string AVStreamId = StreamData.id + std::to_string(static_cast<int>(StreamData.type))
+ std::to_string(StreamData.direction);
std::lock_guard<std::mutex> lk(avStreamsMtx_);
auto it = callAVStreams.find(AVStreamId);
if (it != callAVStreams.end())
return;
it = callAVStreams.insert(it, {AVStreamId, mediaStreamSubject});
streamSource.attachPriorityObserver(it->second);
jami::Manager::instance()
.getJamiPluginManager()
.getCallServicesManager()
.createAVSubject(StreamData, it->second);
}
#endif // ENABLE_PLUGIN
void
SIPCall::setCallMediaLocal()
{
if (localAudioPort_ == 0
#ifdef ENABLE_VIDEO
|| localVideoPort_ == 0
#endif
)
generateMediaPorts();
}
void
SIPCall::generateMediaPorts()
{
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return;
}
// Reference: http://www.cs.columbia.edu/~hgs/rtp/faq.html#ports
// We only want to set ports to new values if they haven't been set
const unsigned callLocalAudioPort = account->generateAudioPort();
if (localAudioPort_ != 0)
account->releasePort(localAudioPort_);
localAudioPort_ = callLocalAudioPort;
sdp_->setLocalPublishedAudioPort(callLocalAudioPort);
#ifdef ENABLE_VIDEO
// https://projects.savoirfairelinux.com/issues/17498
const unsigned int callLocalVideoPort = account->generateVideoPort();
if (localVideoPort_ != 0)
account->releasePort(localVideoPort_);
// this should already be guaranteed by SIPAccount
assert(localAudioPort_ != callLocalVideoPort);
localVideoPort_ = callLocalVideoPort;
sdp_->setLocalPublishedVideoPort(callLocalVideoPort);
#endif
}
void
SIPCall::setContactHeader(pj_str_t* contact)
{
pj_strcpy(&contactHeader_, contact);
}
void
SIPCall::setTransport(const std::shared_ptr<SipTransport>& t)
{
if (isSecure() and t and not t->isSecure()) {
JAMI_ERR("Can't set unsecure transport to secure call.");
return;
}
const auto list_id = reinterpret_cast<uintptr_t>(this);
if (transport_)
transport_->removeStateListener(list_id);
transport_ = t;
if (transport_) {
setSecure(transport_->isSecure());
// listen for transport destruction
transport_->addStateListener(
list_id,
[wthis_ = weak()](pjsip_transport_state state, const pjsip_transport_state_info*) {
if (auto this_ = wthis_.lock()) {
// end the call if the SIP transport is shut down
auto isAlive = SipTransport::isAlive(state);
if (not isAlive
and this_->getConnectionState() != ConnectionState::DISCONNECTED) {
JAMI_WARN(
"[call:%s] Ending call because underlying SIP transport was closed",
this_->getCallId().c_str());
this_->stopAllMedia();
this_->onFailure(ECONNRESET);
}
}
});
}
}
/**
* Send a reINVITE inside an active dialog to modify its state
* Local SDP session should be modified before calling this method
*/
int
SIPCall::SIPSessionReinvite()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
// Do nothing if no invitation processed yet
if (not inv or inv->invite_tsx)
return PJ_SUCCESS;
JAMI_DBG("[call:%s] Processing reINVITE (state=%s)",
getCallId().c_str(),
pjsip_inv_state_name(inv->state));
// Generate new ports to receive the new media stream
// LibAV doesn't discriminate SSRCs and will be confused about Seq changes on a given port
generateMediaPorts();
sdp_->clearIce();
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return !PJ_SUCCESS;
}
if (not sdp_->createOffer(acc->getActiveAccountCodecInfoList(MEDIA_AUDIO),
acc->getActiveAccountCodecInfoList(
acc->isVideoEnabled() and not isAudioOnly() ? MEDIA_VIDEO
: MEDIA_NONE),
acc->getSrtpKeyExchange(),
getState() == CallState::HOLD))
return !PJ_SUCCESS;
if (initIceMediaTransport(true))
setupLocalSDPFromIce();
pjsip_tx_data* tdata;
auto local_sdp = sdp_->getLocalSdpSession();
auto result = pjsip_inv_reinvite(inv.get(), nullptr, local_sdp, &tdata);
if (result == PJ_SUCCESS) {
if (!tdata)
return PJ_SUCCESS;
// Add user-agent header
sip_utils::addUserAgenttHeader(acc->getUserAgentName(), tdata);
result = pjsip_inv_send_msg(inv.get(), tdata);
if (result == PJ_SUCCESS)
return PJ_SUCCESS;
JAMI_ERR("[call:%s] Failed to send REINVITE msg (pjsip: %s)",
getCallId().c_str(),
sip_utils::sip_strerror(result).c_str());
// Canceling internals without sending (anyways the send has just failed!)
pjsip_inv_cancel_reinvite(inv.get(), &tdata);
} else
JAMI_ERR("[call:%s] Failed to create REINVITE msg (pjsip: %s)",
getCallId().c_str(),
sip_utils::sip_strerror(result).c_str());
return !PJ_SUCCESS;
}
void
SIPCall::sendSIPInfo(std::string_view body, std::string_view subtype)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (not inv or not inv->dlg)
throw VoipLinkException("Couldn't get invite dialog");
constexpr pj_str_t methodName = CONST_PJ_STR("INFO");
constexpr pj_str_t type = CONST_PJ_STR("application");
pjsip_method method;
pjsip_method_init_np(&method, (pj_str_t*) &methodName);
/* Create request message. */
pjsip_tx_data* tdata;
if (pjsip_dlg_create_request(inv->dlg, &method, -1, &tdata) != PJ_SUCCESS) {
JAMI_ERR("[call:%s] Could not create dialog", getCallId().c_str());
return;
}
/* Create "application/<subtype>" message body. */
pj_str_t content = CONST_PJ_STR(body);
pj_str_t pj_subtype = CONST_PJ_STR(subtype);
tdata->msg->body = pjsip_msg_body_create(tdata->pool, &type, &pj_subtype, &content);
if (tdata->msg->body == NULL)
pjsip_tx_data_dec_ref(tdata);
else
pjsip_dlg_send_request(inv->dlg, tdata, Manager::instance().sipVoIPLink().getModId(), NULL);
}
void
SIPCall::updateRecState(bool state)
{
std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<media_control><vc_primitive><to_encoder>"
"<recording_state="
+ std::to_string(state)
+ "/>"
"</to_encoder></vc_primitive></media_control>";
// see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
JAMI_DBG("Sending recording state via SIP INFO");
try {
sendSIPInfo(BODY, "media_control+xml");
} catch (const std::exception& e) {
JAMI_ERR("Error sending recording state: %s", e.what());
}
}
void
SIPCall::requestKeyframe()
{
auto now = clock::now();
if ((now - lastKeyFrameReq_) < MS_BETWEEN_2_KEYFRAME_REQUEST
and lastKeyFrameReq_ != time_point::min())
return;
constexpr auto BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<media_control><vc_primitive><to_encoder>"
"<picture_fast_update/>"
"</to_encoder></vc_primitive></media_control>"sv;
JAMI_DBG("Sending video keyframe request via SIP INFO");
try {
sendSIPInfo(BODY, "media_control+xml");
} catch (const std::exception& e) {
JAMI_ERR("Error sending video keyframe request: %s", e.what());
}
lastKeyFrameReq_ = now;
}
void
SIPCall::setMute(bool state)
{
std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<media_control><vc_primitive><to_encoder>"
"<mute_state="
+ std::to_string(state)
+ "/>"
"</to_encoder></vc_primitive></media_control>";
// see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
JAMI_DBG("Sending mute state via SIP INFO");
try {
sendSIPInfo(BODY, "media_control+xml");
} catch (const std::exception& e) {
JAMI_ERR("Error sending mute state: %s", e.what());
}
}
void
SIPCall::updateSDPFromSTUN()
{
JAMI_WARN("[call:%s] SIPCall::updateSDPFromSTUN() not implemented", getCallId().c_str());
}
void
SIPCall::terminateSipSession(int status)
{
JAMI_DBG("[call:%s] Terminate SIP session", getCallId().c_str());
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (inv and inv->state != PJSIP_INV_STATE_DISCONNECTED) {
pjsip_tx_data* tdata = nullptr;
auto ret = pjsip_inv_end_session(inv.get(), status, nullptr, &tdata);
if (ret == PJ_SUCCESS) {
if (tdata) {
auto account = getSIPAccount();
if (account) {
auto contact = account->getContactHeader(transport_ ? transport_->get()
: nullptr);
sip_utils::addContactHeader(&contact, tdata);
} else {
JAMI_ERR("No account detected");
}
// Add user-agent header
sip_utils::addUserAgenttHeader(account->getUserAgentName(), tdata);
ret = pjsip_inv_send_msg(inv.get(), tdata);
if (ret != PJ_SUCCESS)
JAMI_ERR("[call:%s] failed to send terminate msg, SIP error (%s)",
getCallId().c_str(),
sip_utils::sip_strerror(ret).c_str());
}
} else
JAMI_ERR("[call:%s] failed to terminate INVITE@%p, SIP error (%s)",
getCallId().c_str(),
inv.get(),
sip_utils::sip_strerror(ret).c_str());
}
inv.reset();
}
void
SIPCall::answer()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (not inv)
throw VoipLinkException("[call:" + getCallId()
+ "] answer: no invite session for this call");
if (!inv->neg) {
JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
getCallId().c_str());
pjmedia_sdp_session* dummy = 0;
Manager::instance().sipVoIPLink().createSDPOffer(inv.get(), &dummy);
if (account->isStunEnabled())
updateSDPFromSTUN();
}
pj_str_t contact(account->getContactHeader(transport_ ? transport_->get() : nullptr));
setContactHeader(&contact);
pjsip_tx_data* tdata;
if (!inv->last_answer)
throw std::runtime_error("Should only be called for initial answer");
// answer with SDP if no SDP was given in initial invite (i.e. inv->neg is NULL)
if (pjsip_inv_answer(inv.get(),
PJSIP_SC_OK,
NULL,
!inv->neg ? sdp_->getLocalSdpSession() : NULL,
&tdata)
!= PJ_SUCCESS)
throw std::runtime_error("Could not init invite request answer (200 OK)");
// contactStr must stay in scope as long as tdata
if (contactHeader_.slen) {
JAMI_DBG("[call:%s] Answering with contact header: %.*s",
getCallId().c_str(),
(int) contactHeader_.slen,
contactHeader_.ptr);
sip_utils::addContactHeader(&contactHeader_, tdata);
}
// Add user-agent header
sip_utils::addUserAgenttHeader(account->getUserAgentName(), tdata);
if (pjsip_inv_send_msg(inv.get(), tdata) != PJ_SUCCESS) {
inv.reset();
throw std::runtime_error("Could not send invite request answer (200 OK)");
}
setState(CallState::ACTIVE, ConnectionState::CONNECTED);
}
void
SIPCall::hangup(int reason)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (inv and inv->dlg) {
pjsip_route_hdr* route = inv->dlg->route_set.next;
while (route and route != &inv->dlg->route_set) {
char buf[1024];
int printed = pjsip_hdr_print_on(route, buf, sizeof(buf));
if (printed >= 0) {
buf[printed] = '\0';
JAMI_DBG("[call:%s] Route header %s", getCallId().c_str(), buf);
}
route = route->next;
}
const int status = reason
? reason
: inv->state <= PJSIP_INV_STATE_EARLY and inv->role != PJSIP_ROLE_UAC
? PJSIP_SC_CALL_TSX_DOES_NOT_EXIST
: inv->state >= PJSIP_INV_STATE_DISCONNECTED ? PJSIP_SC_DECLINE
: 0;
// Notify the peer
terminateSipSession(status);
}
// Stop all RTP streams
stopAllMedia();
setState(Call::ConnectionState::DISCONNECTED, reason);
dht::ThreadPool::io().run([w = weak()] {
if (auto shared = w.lock())
shared->removeCall();
});
}
void
SIPCall::refuse()
{
if (!isIncoming() or getConnectionState() == ConnectionState::CONNECTED or !inv)
return;
stopAllMedia();
// Notify the peer
terminateSipSession(PJSIP_SC_DECLINE);
setState(Call::ConnectionState::DISCONNECTED, ECONNABORTED);
removeCall();
}
static void
transfer_client_cb(pjsip_evsub* sub, pjsip_event* event)
{
auto mod_ua_id = Manager::instance().sipVoIPLink().getModId();
switch (pjsip_evsub_get_state(sub)) {
case PJSIP_EVSUB_STATE_ACCEPTED:
if (!event)
return;
pj_assert(event->type == PJSIP_EVENT_TSX_STATE
&& event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
break;
case PJSIP_EVSUB_STATE_TERMINATED:
pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
break;
case PJSIP_EVSUB_STATE_ACTIVE: {
if (!event)
return;
pjsip_rx_data* r_data = event->body.rx_msg.rdata;
if (!r_data)
return;
std::string request(pjsip_rx_data_get_info(r_data));
pjsip_status_line status_line = {500, *pjsip_get_status_text(500)};
if (!r_data->msg_info.msg)
return;
if (r_data->msg_info.msg->line.req.method.id == PJSIP_OTHER_METHOD
and request.find("NOTIFY") != std::string::npos) {
pjsip_msg_body* body = r_data->msg_info.msg->body;
if (!body)
return;
if (pj_stricmp2(&body->content_type.type, "message")
or pj_stricmp2(&body->content_type.subtype, "sipfrag"))
return;
if (pjsip_parse_status_line((char*) body->data, body->len, &status_line) != PJ_SUCCESS)
return;
}
if (!r_data->msg_info.cid)
return;
auto call = static_cast<SIPCall*>(pjsip_evsub_get_mod_data(sub, mod_ua_id));
if (!call)
return;
if (status_line.code / 100 == 2) {
if (call->inv)
call->terminateSipSession(PJSIP_SC_GONE);
Manager::instance().hangupCall(call->getCallId());
pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
}
break;
}
case PJSIP_EVSUB_STATE_NULL:
case PJSIP_EVSUB_STATE_SENT:
case PJSIP_EVSUB_STATE_PENDING:
case PJSIP_EVSUB_STATE_UNKNOWN:
default:
break;
}
}
bool
SIPCall::transferCommon(const pj_str_t* dst)
{
if (not inv or not inv->dlg)
return false;
pjsip_evsub_user xfer_cb;
pj_bzero(&xfer_cb, sizeof(xfer_cb));
xfer_cb.on_evsub_state = &transfer_client_cb;
pjsip_evsub* sub;
if (pjsip_xfer_create_uac(inv->dlg, &xfer_cb, &sub) != PJ_SUCCESS)
return false;
/* Associate this voiplink of call with the client subscription
* We can not just associate call with the client subscription
* because after this function, we can no find the cooresponding
* voiplink from the call any more. But the voiplink is useful!
*/
pjsip_evsub_set_mod_data(sub, Manager::instance().sipVoIPLink().getModId(), this);
/*
* Create REFER request.
*/
pjsip_tx_data* tdata;
if (pjsip_xfer_initiate(sub, dst, &tdata) != PJ_SUCCESS)
return false;
/* Send. */
if (pjsip_xfer_send_request(sub, tdata) != PJ_SUCCESS)
return false;
return true;
}
void
SIPCall::transfer(const std::string& to)
{
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (Recordable::isRecording()) {
deinitRecorder();
stopRecording();
}
std::string toUri = account->getToUri(to);
const pj_str_t dst(CONST_PJ_STR(toUri));
JAMI_DBG("[call:%s] Transferring to %.*s", getCallId().c_str(), (int) dst.slen, dst.ptr);
if (!transferCommon(&dst))
throw VoipLinkException("Couldn't transfer");
}
bool
SIPCall::attendedTransfer(const std::string& to)
{
const auto toCall = Manager::instance().callFactory.getCall<SIPCall>(to);
if (!toCall)
return false;
if (not toCall->inv or not toCall->inv->dlg)
return false;
pjsip_dialog* target_dlg = toCall->inv->dlg;
pjsip_uri* uri = (pjsip_uri*) pjsip_uri_get_uri(target_dlg->remote.info->uri);
char str_dest_buf[PJSIP_MAX_URL_SIZE * 2] = {'<'};
pj_str_t dst = {str_dest_buf, 1};
dst.slen += pjsip_uri_print(PJSIP_URI_IN_REQ_URI,
uri,
str_dest_buf + 1,
sizeof(str_dest_buf) - 1);
dst.slen += pj_ansi_snprintf(str_dest_buf + dst.slen,
sizeof(str_dest_buf) - dst.slen,
"?"
"Replaces=%.*s"
"%%3Bto-tag%%3D%.*s"
"%%3Bfrom-tag%%3D%.*s>",
(int) target_dlg->call_id->id.slen,
target_dlg->call_id->id.ptr,
(int) target_dlg->remote.info->tag.slen,
target_dlg->remote.info->tag.ptr,
(int) target_dlg->local.info->tag.slen,
target_dlg->local.info->tag.ptr);
return transferCommon(&dst);
}
bool
SIPCall::onhold(OnReadyCb&& cb)
{
// If ICE is currently negotiating, we must wait before hold the call
if (isWaitingForIceAndMedia_) {
holdCb_ = std::move(cb);
remainingRequest_ = Request::HoldingOn;
return false;
}
auto result = hold();
if (cb)
cb(result);
return result;
}
bool
SIPCall::hold()
{
if (not setState(CallState::HOLD))
return false;
stopAllMedia();
if (getConnectionState() == ConnectionState::CONNECTED) {
if (SIPSessionReinvite() != PJ_SUCCESS) {
JAMI_WARN("[call:%s] Reinvite failed", getCallId().c_str());
return true;
}
}
isWaitingForIceAndMedia_ = true;
return true;
}
bool
SIPCall::offhold(OnReadyCb&& cb)
{
// If ICE is currently negotiating, we must wait before unhold the call
if (isWaitingForIceAndMedia_) {
offHoldCb_ = std::move(cb);
remainingRequest_ = Request::HoldingOff;
return false;
}
auto result = unhold();
if (cb)
cb(result);
return result;
}
bool
SIPCall::unhold()
{
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return false;
}
bool success = false;
try {
if (account->isStunEnabled())
success = internalOffHold([&] { updateSDPFromSTUN(); });
else
success = internalOffHold([] {});
} catch (const SdpException& e) {
JAMI_ERR("[call:%s] %s", getCallId().c_str(), e.what());
throw VoipLinkException("SDP issue in offhold");
}
if (success)
isWaitingForIceAndMedia_ = true;
return success;
}
bool
SIPCall::internalOffHold(const std::function<void()>& sdp_cb)
{
if (not setState(CallState::ACTIVE))
return false;
sdp_cb();
if (getConnectionState() == ConnectionState::CONNECTED) {
if (SIPSessionReinvite() != PJ_SUCCESS) {
JAMI_WARN("[call:%s] resuming hold", getCallId().c_str());
if (isWaitingForIceAndMedia_) {
remainingRequest_ = Request::HoldingOn;
} else {
hold();
}
return false;
}
}
return true;
}
void
SIPCall::switchInput(const std::string& resource)
{
mediaInput_ = resource;
if (isWaitingForIceAndMedia_) {
remainingRequest_ = Request::SwitchInput;
} else {
if (SIPSessionReinvite() == PJ_SUCCESS) {
isWaitingForIceAndMedia_ = true;
}
}
}
void
SIPCall::peerHungup()
{
// Stop all RTP streams
stopAllMedia();
if (inv)
terminateSipSession(PJSIP_SC_NOT_FOUND);
else
JAMI_ERR("[call:%s] peerHungup: no invite session for this call", getCallId().c_str());
Call::peerHungup();
}
void
SIPCall::carryingDTMFdigits(char code)
{
int duration = Manager::instance().voipPreferences.getPulseLength();
char dtmf_body[1000];
int ret;
// handle flash code
if (code == '!') {
ret = snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=16\r\nDuration=%d\r\n", duration);
} else {
ret = snprintf(dtmf_body,
sizeof dtmf_body - 1,
"Signal=%c\r\nDuration=%d\r\n",
code,
duration);
}
try {
sendSIPInfo({dtmf_body, (size_t) ret}, "dtmf-relay");
} catch (const std::exception& e) {
JAMI_ERR("Error sending DTMF: %s", e.what());
}
}
void
SIPCall::setVideoOrientation(int rotation)
{
std::string sip_body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<media_control><vc_primitive><to_encoder>"
"<device_orientation="
+ std::to_string(-rotation)
+ "/>"
"</to_encoder></vc_primitive></media_control>";
JAMI_DBG("Sending device orientation via SIP INFO");
sendSIPInfo(sip_body, "media_control+xml");
}
void
SIPCall::sendTextMessage(const std::map<std::string, std::string>& messages, const std::string& from)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
// TODO: for now we ignore the "from" (the previous implementation for sending this info was
// buggy and verbose), another way to send the original message sender will be implemented
// in the future
if (not subcalls_.empty()) {
pendingOutMessages_.emplace_back(messages, from);
for (auto& c : subcalls_)
c->sendTextMessage(messages, from);
} else {
if (inv) {
try {
im::sendSipMessage(inv.get(), messages);
} catch (...) {
}
} else {
pendingOutMessages_.emplace_back(messages, from);
JAMI_ERR("[call:%s] sendTextMessage: no invite session for this call",
getCallId().c_str());
}
}
}
void
SIPCall::removeCall()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
JAMI_WARN("[call:%s] removeCall()", getCallId().c_str());
if (sdp_) {
sdp_->setActiveLocalSdpSession(nullptr);
sdp_->setActiveRemoteSdpSession(nullptr);
}
Call::removeCall();
mediaTransport_.reset();
tmpMediaTransport_.reset();
inv.reset();
setTransport({});
}
void
SIPCall::onFailure(signed cause)
{
if (setState(CallState::MERROR, ConnectionState::DISCONNECTED, cause)) {
runOnMainThread([w = weak()] {
if (auto shared = w.lock()) {
auto& call = *shared;
Manager::instance().callFailure(call);
call.removeCall();
}
});
}
}
void
SIPCall::onBusyHere()
{
if (getCallType() == CallType::OUTGOING)
setState(CallState::PEER_BUSY, ConnectionState::DISCONNECTED);
else
setState(CallState::BUSY, ConnectionState::DISCONNECTED);
runOnMainThread([w = weak()] {
if (auto shared = w.lock()) {
auto& call = *shared;
Manager::instance().callBusy(call);
call.removeCall();
}
});
}
void
SIPCall::onClosed()
{
runOnMainThread([w = weak()] {
if (auto shared = w.lock()) {
auto& call = *shared;
Manager::instance().peerHungupCall(call);
call.removeCall();
}
});
}
void
SIPCall::onAnswered()
{
JAMI_WARN("[call:%s] onAnswered()", getCallId().c_str());
runOnMainThread([w = weak()] {
if (auto shared = w.lock()) {
if (shared->getConnectionState() != ConnectionState::CONNECTED) {
shared->setState(CallState::ACTIVE, ConnectionState::CONNECTED);
if (not shared->isSubcall()) {
Manager::instance().peerAnsweredCall(*shared);
}
}
}
});
}
void
SIPCall::sendKeyframe()
{
#ifdef ENABLE_VIDEO
dht::ThreadPool::computation().run([w = weak()] {
if (auto sthis = w.lock()) {
JAMI_DBG("handling picture fast update request");
sthis->getVideoRtp().forceKeyFrame();
}
});
#endif
}
void
SIPCall::onPeerRinging()
{
setState(ConnectionState::RINGING);
}
void
SIPCall::setupLocalSDPFromIce()
{
auto media_tr = getIceMediaTransport();
if (not media_tr) {
JAMI_WARN("[call:%s] no media ICE transport, SDP not changed", getCallId().c_str());
return;
}
// we need an initialized ICE to progress further
if (media_tr->waitForInitialization(DEFAULT_ICE_INIT_TIMEOUT) <= 0) {
JAMI_ERR("[call:%s] Medias' ICE init failed", getCallId().c_str());
return;
}
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (!sdp_) {
JAMI_ERR("No sdp detected");
return;
}
JAMI_WARN("[call:%s] fill SDP with ICE transport %p", getCallId().c_str(), media_tr);
sdp_->addIceAttributes(media_tr->getLocalAttributes());
// Add video and audio channels
sdp_->addIceCandidates(SDP_AUDIO_MEDIA_ID, media_tr->getLocalCandidates(ICE_AUDIO_RTP_COMPID));
sdp_->addIceCandidates(SDP_AUDIO_MEDIA_ID, media_tr->getLocalCandidates(ICE_AUDIO_RTCP_COMPID));
#ifdef ENABLE_VIDEO
sdp_->addIceCandidates(SDP_VIDEO_MEDIA_ID, media_tr->getLocalCandidates(ICE_VIDEO_RTP_COMPID));
sdp_->addIceCandidates(SDP_VIDEO_MEDIA_ID, media_tr->getLocalCandidates(ICE_VIDEO_RTCP_COMPID));
#endif
}
std::vector<IceCandidate>
SIPCall::getAllRemoteCandidates()
{
auto media_tr = getIceMediaTransport();
if (not media_tr) {
JAMI_WARN("[call:%s] no media ICE transport", getCallId().c_str());
return {};
}
auto addSDPCandidates = [&, this](unsigned sdpMediaId, std::vector<IceCandidate>& out) {
IceCandidate cand;
if (!sdp_)
return;
for (auto& line : sdp_->getIceCandidates(sdpMediaId)) {
if (media_tr->getCandidateFromSDP(line, cand)) {
JAMI_DBG("[call:%s] add remote ICE candidate: %s",
getCallId().c_str(),
line.c_str());
out.emplace_back(cand);
}
}
};
std::vector<IceCandidate> rem_candidates;
addSDPCandidates(SDP_AUDIO_MEDIA_ID, rem_candidates);
#ifdef ENABLE_VIDEO
addSDPCandidates(SDP_VIDEO_MEDIA_ID, rem_candidates);
#endif
return rem_candidates;
}
std::shared_ptr<AccountCodecInfo>
SIPCall::getVideoCodec() const
{
#ifdef ENABLE_VIDEO
return videortp_->getCodec();
#endif
return {};
}
std::shared_ptr<AccountCodecInfo>
SIPCall::getAudioCodec() const
{
return avformatrtp_->getCodec();
}
void
SIPCall::startAllMedia()
{
if (!transport_ or !sdp_)
return;
JAMI_WARN("[call:%s] startAllMedia()", getCallId().c_str());
if (isSecure() && not transport_->isSecure()) {
JAMI_ERR("[call:%s] Can't perform secure call over insecure SIP transport",
getCallId().c_str());
onFailure(EPROTONOSUPPORT);
return;
}
auto slots = sdp_->getMediaSlots();
unsigned ice_comp_id = 0;
bool peer_holding {true};
int slotN = -1;
#ifdef ENABLE_VIDEO
videortp_->setChangeOrientationCallback([wthis = weak()](int angle) {
runOnMainThread([wthis, angle] {
if (auto this_ = wthis.lock())
this_->setVideoOrientation(angle);
});
});
#endif
for (const auto& slot : slots) {
++slotN;
const auto& local = slot.first;
const auto& remote = slot.second;
if (local.type != remote.type) {
JAMI_ERR("[call:%s] [SDP:slot#%u] Inconsistent media types between local and remote",
getCallId().c_str(),
slotN);
continue;
}
RtpSession* rtp = local.type == MEDIA_AUDIO ? static_cast<RtpSession*>(avformatrtp_.get())
#ifdef ENABLE_VIDEO
: static_cast<RtpSession*>(videortp_.get());
#else
: nullptr;
#endif
if (not rtp)
continue;
if (!local.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing local codec", getCallId().c_str(), slotN);
continue;
}
if (!remote.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing remote codec", getCallId().c_str(), slotN);
continue;
}
peer_holding &= remote.holding;
if (isSecure() && (not local.crypto || not remote.crypto)) {
JAMI_ERR(
"[call:%s] [SDP:slot#%u] Can't perform secure call over insecure RTP transport",
getCallId().c_str(),
slotN);
continue;
}
auto new_mtu = transport_->getTlsMtu();
if (local.type & MEDIA_AUDIO)
avformatrtp_->switchInput(mediaInput_);
avformatrtp_->setMtu(new_mtu);
#ifdef ENABLE_VIDEO
if (local.type & MEDIA_VIDEO)
videortp_->switchInput(mediaInput_);
videortp_->setMtu(new_mtu);
#endif
rtp->updateMedia(remote, local);
rtp->setSuccessfulSetupCb([this](MediaType type) { rtpSetupSuccess(type); });
#ifdef ENABLE_VIDEO
videortp_->setRequestKeyFrameCallback([wthis = weak()] {
runOnMainThread([wthis] {
if (auto this_ = wthis.lock())
this_->requestKeyframe();
});
});
#endif
// Not restarting media loop on hold as it's a huge waste of CPU ressources
// because of the audio loop
if (getState() != CallState::HOLD) {
if (isIceRunning()) {
rtp->start(newIceSocket(ice_comp_id + 0), newIceSocket(ice_comp_id + 1));
ice_comp_id += 2;
} else
rtp->start(nullptr, nullptr);
}
switch (local.type) {
#ifdef ENABLE_VIDEO
case MEDIA_VIDEO:
isVideoMuted_ = mediaInput_.empty();
break;
#endif
default:
break;
}
}
if (not isSubcall() and peerHolding_ != peer_holding) {
peerHolding_ = peer_holding;
emitSignal<DRing::CallSignal::PeerHold>(getCallId(), peerHolding_);
}
// Media is restarted, we can process the last holding request.
isWaitingForIceAndMedia_ = false;
if (remainingRequest_ != Request::NoRequest) {
bool result = true;
switch (remainingRequest_) {
case Request::HoldingOn:
result = hold();
if (holdCb_) {
holdCb_(result);
holdCb_ = nullptr;
}
break;
case Request::HoldingOff:
result = unhold();
if (offHoldCb_) {
offHoldCb_(result);
offHoldCb_ = nullptr;
}
break;
case Request::SwitchInput:
SIPSessionReinvite();
break;
default:
break;
}
remainingRequest_ = Request::NoRequest;
}
#ifdef ENABLE_PLUGIN
// Create AVStreams associated with the call
createCallAVStreams();
#endif
}
void
SIPCall::restartMediaSender()
{
JAMI_DBG("[call:%s] restarting TX media streams", getCallId().c_str());
avformatrtp_->restartSender();
#ifdef ENABLE_VIDEO
videortp_->restartSender();
#endif
}
void
SIPCall::stopAllMedia()
{
JAMI_DBG("[call:%s] stopping all medias", getCallId().c_str());
if (Recordable::isRecording()) {
deinitRecorder();
stopRecording(); // if call stops, finish recording
}
avformatrtp_->stop();
#ifdef ENABLE_VIDEO
videortp_->stop();
#endif
#ifdef ENABLE_PLUGIN
{
std::lock_guard<std::mutex> lk(avStreamsMtx_);
callAVStreams.erase(getCallId() + "00"); // audio out
callAVStreams.erase(getCallId() + "01"); // audio in
callAVStreams.erase(getCallId() + "10"); // video out
callAVStreams.erase(getCallId() + "11"); // video in
}
jami::Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject(
getCallId());
#endif
}
void
SIPCall::muteMedia(const std::string& mediaType, bool mute)
{
if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_VIDEO) == 0) {
#ifdef ENABLE_VIDEO
if (mute == isVideoMuted_)
return;
JAMI_WARN("[call:%s] video muting %s", getCallId().c_str(), bool_to_str(mute));
isVideoMuted_ = mute;
mediaInput_ = isVideoMuted_ ? ""
: Manager::instance()
.getVideoManager()
.videoDeviceMonitor.getMRLForDefaultDevice();
DRing::switchInput(getCallId(), mediaInput_);
if (not isSubcall())
emitSignal<DRing::CallSignal::VideoMuted>(getCallId(), isVideoMuted_);
#endif
} else if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_AUDIO) == 0) {
if (mute == isAudioMuted_)
return;
JAMI_WARN("[call:%s] audio muting %s", getCallId().c_str(), bool_to_str(mute));
isAudioMuted_ = mute;
avformatrtp_->setMuted(isAudioMuted_);
if (not isSubcall())
emitSignal<DRing::CallSignal::AudioMuted>(getCallId(), isAudioMuted_);
setMute(mute);
}
}
/// \brief Prepare media transport and launch media stream based on negotiated SDP
///
/// This method has to be called by link (ie SipVoIpLink) when SDP is negotiated and
/// media streams structures are knows.
/// In case of ICE transport used, the medias streams are launched asynchonously when
/// the transport is negotiated.
void
SIPCall::onMediaUpdate()
{
JAMI_WARN("[call:%s] medias changed", getCallId().c_str());
// Main call (no subcalls) must wait for ICE now, the rest of code needs to access
// to a negotiated transport.
runOnMainThread([w = weak()] {
if (auto this_ = w.lock()) {
std::lock_guard<std::recursive_mutex> lk {this_->callMutex_};
// The call is already ended, so we don't need to restart medias
if (!this_->inv or this_->inv->state == PJSIP_INV_STATE_DISCONNECTED or not this_->sdp_)
return;
// If ICE is not used, start medias now
auto rem_ice_attrs = this_->sdp_->getIceAttributes();
if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
JAMI_WARN("[call:%s] no remote ICE for medias", this_->getCallId().c_str());
this_->stopAllMedia();
this_->startAllMedia();
return;
}
if (not this_->isSubcall())
this_->startIceMedia();
}
});
}
void
SIPCall::startIceMedia()
{
auto ice = getIceMediaTransport();
if (not ice or ice->isFailed()) {
JAMI_ERR("[call:%s] Media ICE init failed", getCallId().c_str());
onFailure(EIO);
return;
}
if (ice->isStarted()) {
// NOTE: for incoming calls, the ice is already there and running
if (ice->isRunning())
onIceNegoSucceed();
return;
}
if (!ice->isInitialized()) {
// In this case, onInitDone will occurs after the startIceMedia
waitForIceInit_ = true;
return;
}
// Start transport on SDP data and wait for negotiation
if (!sdp_)
return;
auto rem_ice_attrs = sdp_->getIceAttributes();
if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
JAMI_ERR("[call:%s] Media ICE attributes empty", getCallId().c_str());
onFailure(EIO);
return;
}
if (not ice->startIce(rem_ice_attrs, getAllRemoteCandidates())) {
JAMI_ERR("[call:%s] Media ICE start failed", getCallId().c_str());
onFailure(EIO);
}
}
void
SIPCall::onIceNegoSucceed()
{
// Nego succeed: move to the new media transport
stopAllMedia();
{
std::lock_guard<std::mutex> lk(transportMtx_);
if (tmpMediaTransport_) {
// Destroy the ICE media transport on another thread. This can take
// quite some time.
if (mediaTransport_)
dht::ThreadPool::io().run([ice = std::move(mediaTransport_)] {});
mediaTransport_ = std::move(tmpMediaTransport_);
}
}
startAllMedia();
}
void
SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer)
{
if (!sdp_)
return;
sdp_->clearIce();
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return;
}
sdp_->receiveOffer(offer,
acc->getActiveAccountCodecInfoList(MEDIA_AUDIO),
acc->getActiveAccountCodecInfoList(acc->isVideoEnabled() ? MEDIA_VIDEO
: MEDIA_NONE),
acc->getSrtpKeyExchange(),
getState() == CallState::HOLD);
setRemoteSdp(offer);
sdp_->startNegotiation();
pjsip_inv_set_sdp_answer(inv.get(), sdp_->getLocalSdpSession());
openPortsUPnP();
}
void
SIPCall::openPortsUPnP()
{
if (upnp_ and sdp_) {
/**
* Try to open the desired ports with UPnP,
* if they are used, use the alternative port and update the SDP session with the newly
* chosen port(s)
*
* TODO:
* No need to request mappings for specfic port numbers. Set the port to '0' to
* request the first available port (faster and more likely to succeed).
*/
JAMI_DBG("[call:%s] opening ports via UPNP for SDP session", getCallId().c_str());
upnp_->reserveMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP);
// RTCP port.
upnp_->reserveMapping(sdp_->getLocalAudioControlPort(), upnp::PortType::UDP);
#ifdef ENABLE_VIDEO
// RTP port.
upnp_->reserveMapping(sdp_->getLocalVideoPort(), upnp::PortType::UDP);
// RTCP port.
upnp_->reserveMapping(sdp_->getLocalVideoControlPort(), upnp::PortType::UDP);
#endif
}
}
std::map<std::string, std::string>
SIPCall::getDetails() const
{
auto details = Call::getDetails();
details.emplace(DRing::Call::Details::PEER_HOLDING, peerHolding_ ? TRUE_STR : FALSE_STR);
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return {};
}
#ifdef ENABLE_VIDEO
// If Video is not enabled return an empty string
details.emplace(DRing::Call::Details::VIDEO_SOURCE, acc->isVideoEnabled() ? mediaInput_ : "");
if (auto codec = videortp_->getCodec())
details.emplace(DRing::Call::Details::VIDEO_CODEC, codec->systemCodecInfo.name);
#endif
#if HAVE_RINGNS
if (not peerRegisteredName_.empty())
details.emplace(DRing::Call::Details::REGISTERED_NAME, peerRegisteredName_);
#endif
#ifdef ENABLE_CLIENT_CERT
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (transport_ and transport_->isSecure()) {
const auto& tlsInfos = transport_->getTlsInfos();
if (tlsInfos.cipher != PJ_TLS_UNKNOWN_CIPHER) {
const auto& cipher = pj_ssl_cipher_name(tlsInfos.cipher);
details.emplace(DRing::TlsTransport::TLS_CIPHER, cipher ? cipher : "");
} else {
details.emplace(DRing::TlsTransport::TLS_CIPHER, "");
}
if (tlsInfos.peerCert) {
details.emplace(DRing::TlsTransport::TLS_PEER_CERT, tlsInfos.peerCert->toString());
auto ca = tlsInfos.peerCert->issuer;
unsigned n = 0;
while (ca) {
std::ostringstream name_str;
name_str << DRing::TlsTransport::TLS_PEER_CA_ << n++;
details.emplace(name_str.str(), ca->toString());
ca = ca->issuer;
}
details.emplace(DRing::TlsTransport::TLS_PEER_CA_NUM, std::to_string(n));
} else {
details.emplace(DRing::TlsTransport::TLS_PEER_CERT, "");
details.emplace(DRing::TlsTransport::TLS_PEER_CA_NUM, "");
}
}
#endif
return details;
}
bool
SIPCall::toggleRecording()
{
pendingRecord_ = true;
if (not readyToRecord_)
return true;
// add streams to recorder before starting the record
if (not Call::isRecording()) {
updateRecState(true);
std::stringstream ss;
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return false;
}
ss << "Conversation at %TIMESTAMP between " << account->getUserUri() << " and " << peerUri_;
recorder_->setMetadata(ss.str(), ""); // use default description
if (avformatrtp_)
avformatrtp_->initRecorder(recorder_);
#ifdef ENABLE_VIDEO
if (!isAudioOnly_ && videortp_)
videortp_->initRecorder(recorder_);
#endif
} else {
updateRecState(false);
deinitRecorder();
}
pendingRecord_ = false;
return Call::toggleRecording();
}
void
SIPCall::deinitRecorder()
{
if (Call::isRecording()) {
if (avformatrtp_)
avformatrtp_->deinitRecorder(recorder_);
#ifdef ENABLE_VIDEO
if (!isAudioOnly_ && videortp_)
videortp_->deinitRecorder(recorder_);
#endif
}
}
void
SIPCall::setSecure(bool sec)
{
if (srtpEnabled_)
return;
if (sec && getConnectionState() != ConnectionState::DISCONNECTED) {
throw std::runtime_error("Can't enable security since call is already connected");
}
srtpEnabled_ = sec;
}
void
SIPCall::InvSessionDeleter::operator()(pjsip_inv_session* inv) const noexcept
{
// prevent this from getting accessed in callbacks
// JAMI_WARN: this is not thread-safe!
if (!inv)
return;
inv->mod_data[Manager::instance().sipVoIPLink().getModId()] = nullptr;
// NOTE: the counter is incremented by sipvoiplink (transaction_request_cb)
pjsip_inv_dec_ref(inv);
}
bool
SIPCall::initIceMediaTransport(bool master,
std::optional<IceTransportOptions> options,
unsigned channel_num)
{
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return false;
}
JAMI_DBG("[call:%s] create media ICE transport", getCallId().c_str());
auto iceOptions = options == std::nullopt ? acc->getIceOptions() : *options;
auto optOnInitDone = std::move(iceOptions.onInitDone);
auto optOnNegoDone = std::move(iceOptions.onNegoDone);
iceOptions.onInitDone = [w = weak(), cb = std::move(optOnInitDone)](bool ok) {
runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
auto call = w.lock();
if (cb)
cb(ok);
if (!ok or !call or !call->waitForIceInit_.exchange(false))
return;
std::lock_guard<std::recursive_mutex> lk {call->callMutex_};
auto rem_ice_attrs = call->sdp_->getIceAttributes();
// Init done but no remote_ice_attributes, the ice->start will be triggered lated
if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty())
return;
call->startIceMedia();
});
};
iceOptions.onNegoDone = [w = weak(), cb = std::move(optOnNegoDone)](bool ok) {
runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
if (cb)
cb(ok);
if (auto call = w.lock()) {
// The ICE is related to subcalls, but medias are handled by parent call
std::lock_guard<std::recursive_mutex> lk {call->callMutex_};
call = call->isSubcall() ? std::dynamic_pointer_cast<SIPCall>(call->parent_) : call;
if (!ok) {
JAMI_ERR("[call:%s] Media ICE negotiation failed", call->getCallId().c_str());
call->onFailure(EIO);
return;
}
call->onIceNegoSucceed();
}
});
};
auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
auto transport = iceTransportFactory.createUTransport(getCallId().c_str(),
channel_num,
master,
iceOptions);
std::lock_guard<std::mutex> lk(transportMtx_);
// Destroy old ice on a separate io pool
if (tmpMediaTransport_)
dht::ThreadPool::io().run([ice = std::make_shared<decltype(tmpMediaTransport_)>(
std::move(tmpMediaTransport_))] {});
tmpMediaTransport_ = std::move(transport);
if (tmpMediaTransport_) {
JAMI_DBG("[call:%s] Successfully created media ICE transport", getCallId().c_str());
} else {
JAMI_ERR("[call:%s] Failed to create media ICE transport", getCallId().c_str());
}
return static_cast<bool>(tmpMediaTransport_);
}
void
SIPCall::merge(Call& call)
{
JAMI_DBG("[sipcall:%s] merge subcall %s", getCallId().c_str(), call.getCallId().c_str());
// This static cast is safe as this method is private and overload Call::merge
auto& subcall = static_cast<SIPCall&>(call);
std::lock(callMutex_, subcall.callMutex_);
std::lock_guard<std::recursive_mutex> lk1 {callMutex_, std::adopt_lock};
std::lock_guard<std::recursive_mutex> lk2 {subcall.callMutex_, std::adopt_lock};
inv = std::move(subcall.inv);
inv->mod_data[Manager::instance().sipVoIPLink().getModId()] = this;
setTransport(std::move(subcall.transport_));
sdp_ = std::move(subcall.sdp_);
peerHolding_ = subcall.peerHolding_;
upnp_ = std::move(subcall.upnp_);
std::copy_n(subcall.contactBuffer_, PJSIP_MAX_URL_SIZE, contactBuffer_);
pj_strcpy(&contactHeader_, &subcall.contactHeader_);
localAudioPort_ = subcall.localAudioPort_;
localVideoPort_ = subcall.localVideoPort_;
{
std::lock_guard<std::mutex> lk(transportMtx_);
mediaTransport_ = std::move(subcall.mediaTransport_);
tmpMediaTransport_ = std::move(subcall.tmpMediaTransport_);
}
Call::merge(subcall);
startIceMedia();
}
void
SIPCall::setRemoteSdp(const pjmedia_sdp_session* sdp)
{
if (!sdp)
return;
// If ICE is not used, start medias now
auto rem_ice_attrs = sdp_->getIceAttributes();
if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
JAMI_WARN("[call:%s] no ICE data in remote SDP", getCallId().c_str());
return;
}
if (not initIceMediaTransport(false)) {
// Fatal condition
// TODO: what's SIP rfc says about that?
// (same question in startIceMedia)
onFailure(EIO);
return;
}
// WARNING: This call blocks! (need ice init done)
setupLocalSDPFromIce();
}
bool
SIPCall::isIceRunning() const
{
return mediaTransport_ and mediaTransport_->isRunning();
}
std::unique_ptr<IceSocket>
SIPCall::newIceSocket(unsigned compId)
{
return std::unique_ptr<IceSocket> {new IceSocket(mediaTransport_, compId)};
}
void
SIPCall::rtpSetupSuccess(MediaType type)
{
if ((not isAudioOnly() && type == MEDIA_VIDEO) || (isAudioOnly() && type == MEDIA_AUDIO))
readyToRecord_ = true;
if (pendingRecord_ && readyToRecord_)
toggleRecording();
}
void
SIPCall::setRemoteRecording(bool state)
{
const std::string& id = getConfId().empty() ? getCallId() : getConfId();
if (state) {
JAMI_WARN("SIP remote recording enabled");
emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), true);
} else {
JAMI_WARN("SIP remote recording disabled");
emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), false);
}
peerRecording_ = state;
}
void
SIPCall::setPeerMute(bool state)
{
if (state) {
JAMI_WARN("SIP Peer muted");
} else {
JAMI_WARN("SIP Peer ummuted");
}
peerMuted_ = state;
if (auto conf = Manager::instance().getConferenceFromID(getConfId())) {
conf->updateMuted();
}
}
} // namespace jami