Select Git revision
dht_proxy_client.cpp
-
Adrien Béraud authored
Use shared flags to avoid blocking for the callback while respecting the callback return value.
Adrien Béraud authoredUse shared flags to avoid blocking for the callback while respecting the callback return value.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
sipcall.cpp 100.93 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"
#include "sipaccountbase.h"
#include "sipvoiplink.h"
#include "logger.h"
#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 "jami/call_const.h"
#include "jami/media_const.h"
#include "client/ring_signal.h"
#include "ice_transport.h"
#include "pjsip-ua/sip_inv.h"
#ifdef ENABLE_PLUGIN
#include "plugin/jamipluginmanager.h"
#endif
#ifdef ENABLE_VIDEO
#include "client/videomanager.h"
#include "video/video_rtp_session.h"
#include "jami/videomanager_interface.h"
#include <chrono>
#include <libavutil/display.h>
#include <video/sinkclient.h>
#endif
#include "audio/ringbufferpool.h"
#include "jamidht/channeled_transport.h"
#include "errno.h"
#include <opendht/crypto.h>
#include <opendht/thread_pool.h>
#include <fmt/ranges.h>
namespace jami {
using sip_utils::CONST_PJ_STR;
using namespace DRing::Call;
#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::milliseconds EXPECTED_ICE_INIT_MAX_TIME {5000};
static constexpr std::chrono::seconds DEFAULT_ICE_NEGO_TIMEOUT {60}; // seconds
static constexpr std::chrono::milliseconds MS_BETWEEN_2_KEYFRAME_REQUEST {1000};
static constexpr int ICE_COMP_ID_RTP {1};
static constexpr int ICE_COMP_COUNT_PER_STREAM {2};
static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR = "10.0.2"sv;
static const std::vector<unsigned> MULTISTREAM_REQUIRED_VERSION
= split_string_to_unsigned(MULTISTREAM_REQUIRED_VERSION_STR, '.');
SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
const std::string& callId,
Call::CallType type,
const std::map<std::string, std::string>& details)
: Call(account, callId, type, details)
, sdp_(new Sdp(callId))
, enableIce_(account->isIceForMediaEnabled())
, srtpEnabled_(account->isSrtpEnabled())
{
if (account->getUPnPActive())
upnp_.reset(new upnp::Controller());
setCallMediaLocal();
// Set the media caps.
sdp_->setLocalMediaCapabilities(MediaType::MEDIA_AUDIO,
account->getActiveAccountCodecInfoList(MEDIA_AUDIO));
#ifdef ENABLE_VIDEO
sdp_->setLocalMediaCapabilities(MediaType::MEDIA_VIDEO,
account->getActiveAccountCodecInfoList(MEDIA_VIDEO));
#endif
auto mediaAttrList = getSIPAccount()->createDefaultMediaList(getSIPAccount()->isVideoEnabled()
and not isAudioOnly(),
getState() == CallState::HOLD);
JAMI_DBG("[call:%s] Create a new [%s] SIP call with %lu media",
getCallId().c_str(),
type == Call::CallType::INCOMING
? "INCOMING"
: (type == Call::CallType::OUTGOING ? "OUTGOING" : "MISSED"),
mediaAttrList.size());
initMediaStreams(mediaAttrList);
}
SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
const std::string& callId,
Call::CallType type,
const std::vector<DRing::MediaMap>& mediaList)
: Call(account, callId, type)
, peerSupportMultiStream_(false)
, sdp_(new Sdp(callId))
, enableIce_(account->isIceForMediaEnabled())
, srtpEnabled_(account->isSrtpEnabled())
{
if (account->getUPnPActive())
upnp_.reset(new upnp::Controller());
setCallMediaLocal();
// Set the media caps.
sdp_->setLocalMediaCapabilities(MediaType::MEDIA_AUDIO,
account->getActiveAccountCodecInfoList(MEDIA_AUDIO));
#ifdef ENABLE_VIDEO
sdp_->setLocalMediaCapabilities(MediaType::MEDIA_VIDEO,
account->getActiveAccountCodecInfoList(MEDIA_VIDEO));
#endif
auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
if (mediaAttrList.size() == 0) {
if (type_ == Call::CallType::INCOMING) {
// Handle incoming call without media offer.
JAMI_WARN(
"[call:%s] No media offered in the incoming invite. An offer will be provided in "
"the answer",
getCallId().c_str());
mediaAttrList = getSIPAccount()->createDefaultMediaList(false,
getState() == CallState::HOLD);
} else {
JAMI_WARN("[call:%s] Creating an outgoing call with empty offer", getCallId().c_str());
}
}
JAMI_DBG("[call:%s] Create a new [%s] SIP call with %lu media",
getCallId().c_str(),
type == Call::CallType::INCOMING
? "INCOMING"
: (type == Call::CallType::OUTGOING ? "OUTGOING" : "MISSED"),
mediaList.size());
initMediaStreams(mediaAttrList);
}
SIPCall::~SIPCall()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
setTransport({});
setInviteSession(); // prevents callback usage
}
size_t
SIPCall::findRtpStreamIndex(const std::string& label) const
{
const auto iter = std::find_if(rtpStreams_.begin(),
rtpStreams_.end(),
[&label](const RtpStream& rtp) {
return label == rtp.mediaAttribute_->label_;
});
// Return the index if there is a match.
if (iter != rtpStreams_.end())
return std::distance(rtpStreams_.begin(), iter);
// No match found.
return rtpStreams_.size();
}
void
SIPCall::createRtpSession(RtpStream& stream)
{
if (not stream.mediaAttribute_)
throw std::runtime_error("Missing media attribute");
if (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO) {
stream.rtpSession_ = std::make_shared<AudioRtpSession>(id_);
}
#ifdef ENABLE_VIDEO
else if (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO) {
stream.rtpSession_ = std::make_shared<video::VideoRtpSession>(id_, getVideoSettings());
}
#endif
else {
throw std::runtime_error("Unsupported media type");
}
// Must be valid at this point.
if (not stream.rtpSession_)
throw std::runtime_error("Failed to create RTP Session");
;
}
void
SIPCall::configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession,
const std::shared_ptr<MediaAttribute>& mediaAttr,
const MediaDescription& localMedia,
const MediaDescription& remoteMedia)
{
JAMI_DBG("[call:%s] Configuring [%s] rtp session",
getCallId().c_str(),
MediaAttribute::mediaTypeToString(mediaAttr->type_));
if (not rtpSession)
throw std::runtime_error("Must have a valid RTP Session");
// Configure the media stream
auto new_mtu = transport_->getTlsMtu();
rtpSession->setMtu(new_mtu);
rtpSession->updateMedia(remoteMedia, localMedia);
// Mute/un-mute media
if (mediaAttr->muted_) {
rtpSession->setMuted(true);
// TODO. Setting mute to true should be enough to mute.
// Kept for backward compatiblity.
rtpSession->setMediaSource("");
} else {
rtpSession->setMuted(false);
rtpSession->setMediaSource(mediaAttr->sourceUri_);
}
rtpSession->setSuccessfulSetupCb([w = weak()](MediaType type, bool isRemote) {
if (auto thisPtr = w.lock())
thisPtr->rtpSetupSuccess(type, isRemote);
});
#ifdef ENABLE_VIDEO
if (localMedia.type == MediaType::MEDIA_VIDEO) {
auto videoRtp = std::dynamic_pointer_cast<video::VideoRtpSession>(rtpSession);
assert(videoRtp);
videoRtp->setRequestKeyFrameCallback([w = weak()] {
runOnMainThread([w] {
if (auto thisPtr = w.lock())
thisPtr->requestKeyframe();
});
});
videoRtp->setChangeOrientationCallback([w = weak()](int angle) {
runOnMainThread([w, angle] {
if (auto thisPtr = w.lock())
thisPtr->setVideoOrientation(angle);
});
});
}
#endif
}
std::shared_ptr<SIPAccountBase>
SIPCall::getSIPAccount() const
{
return std::static_pointer_cast<SIPAccountBase>(getAccount().lock());
}
#ifdef ENABLE_PLUGIN
void
SIPCall::createCallAVStreams()
{
if (hasVideo()) {
auto videoRtp = getVideoRtp();
if (not videoRtp)
return;
if (videoRtp->hasConference()) {
clearCallAVStreams();
return;
}
}
auto baseId = getCallId();
/**
* 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();
};
auto const& audioRtp = getAudioRtp();
if (not audioRtp) {
throw std::runtime_error("Must have a valid Audio RTP Session");
}
// Preview
if (auto& localAudio = audioRtp->getAudioLocal()) {
auto previewSubject = std::make_shared<MediaStreamSubject>(audioMap);
StreamData microStreamData {baseId, false, StreamType::audio, getPeerNumber()};
createCallAVStream(microStreamData, *localAudio, previewSubject);
}
// Receive
if (auto& audioReceive = audioRtp->getAudioReceive()) {
auto receiveSubject = std::make_shared<MediaStreamSubject>(audioMap);
StreamData phoneStreamData {baseId, true, StreamType::audio, getPeerNumber()};
createCallAVStream(phoneStreamData, (AVMediaStream&) *audioReceive, receiveSubject);
}
#ifdef ENABLE_VIDEO
if (hasVideo()) {
auto videoRtp = getVideoRtp();
if (not videoRtp)
return;
// Map: maps the VideoFrame to an AVFrame
auto map = [](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>(map);
StreamData previewStreamData {getCallId(), false, StreamType::video, getPeerNumber()};
createCallAVStream(previewStreamData, *videoPreview, previewSubject);
}
// Receive
if (auto& videoReceive = videoRtp->getVideoReceive()) {
auto receiveSubject = std::make_shared<MediaStreamSubject>(map);
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);
}
void
SIPCall::clearCallAVStreams()
{
std::lock_guard<std::mutex> lk(avStreamsMtx_);
callAVStreams.clear();
}
#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;
}
// TODO. Setting specfic range for RTP ports is obsolete, in
// particular in the context of ICE.
// 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_->setLocalPublishedAudioPorts(callLocalAudioPort,
rtcpMuxEnabled_ ? 0 : callLocalAudioPort + 1);
#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_->setLocalPublishedVideoPorts(callLocalVideoPort,
rtcpMuxEnabled_ ? 0 : callLocalVideoPort + 1);
#endif
}
void
SIPCall::setContactHeader(pj_str_t contact)
{
pj_strcpy(&contactHeader_, &contact);
}
void
SIPCall::setTransport(const std::shared_ptr<SipTransport>& t)
{
if (t != transport_) {
JAMI_WARN("[call:%s] Setting tranport to [%p]", getCallId().c_str(), t.get());
}
transport_ = t;
if (not t) {
return;
}
if (isSrtpEnabled() and not transport_->isSecure()) {
JAMI_WARN("[call:%s] Crypto (SRTP) is negotiated over an un-encrypted signaling channel",
getCallId().c_str());
}
if (not isSrtpEnabled() and transport_->isSecure()) {
JAMI_WARN("[call:%s] The signaling channel is encrypted but the media is not encrypted",
getCallId().c_str());
}
const auto list_id = reinterpret_cast<uintptr_t>(this);
transport_->removeStateListener(list_id);
// listen for transport destruction
transport_->addStateListener(
list_id, [wthis_ = weak()](pjsip_transport_state state, const pjsip_transport_state_info*) {
if (auto this_ = wthis_.lock()) {
JAMI_DBG("[call:%s] SIP transport state [%i] - connection state [%u]",
this_->getCallId().c_str(),
state,
static_cast<unsigned>(this_->getConnectionState()));
// End the call if the SIP transport was 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);
}
}
});
}
void
SIPCall::requestReinvite()
{
JAMI_DBG("[call:%s] Sending a SIP re-invite to request media change", getCallId().c_str());
if (isWaitingForIceAndMedia_) {
remainingRequest_ = Request::SwitchInput;
} else {
auto mediaList = getMediaAttributeList();
assert(not mediaList.empty());
// TODO. We should erase existing streams only after the new
// ones were successfully negotiated, and make a live switch. But
// for now, we reset all streams before creating new ones.
rtpStreams_.clear();
initMediaStreams(mediaList);
if (SIPSessionReinvite(mediaList) == PJ_SUCCESS) {
isWaitingForIceAndMedia_ = true;
}
}
}
/**
* Send a reINVITE inside an active dialog to modify its state
* Local SDP session should be modified before calling this method
*/
int
SIPCall::SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList)
{
assert(not mediaAttrList.empty());
std::lock_guard<std::recursive_mutex> lk {callMutex_};
// Do nothing if no invitation processed yet
if (not inviteSession_ or inviteSession_->invite_tsx)
return PJ_SUCCESS;
JAMI_DBG("[call:%s] Preparing and sending a re-invite (state=%s)",
getCallId().c_str(),
pjsip_inv_state_name(inviteSession_->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 (not acc) {
JAMI_ERR("No account detected");
return !PJ_SUCCESS;
}
if (not sdp_->createOffer(mediaAttrList))
return !PJ_SUCCESS;
if (isIceEnabled()) {
createIceMediaTransport();
if (initIceMediaTransport(true))
addLocalIceAttributes();
}
pjsip_tx_data* tdata;
auto local_sdp = sdp_->getLocalSdpSession();
auto result = pjsip_inv_reinvite(inviteSession_.get(), nullptr, local_sdp, &tdata);
if (result == PJ_SUCCESS) {
if (!tdata)
return PJ_SUCCESS;
// Add user-agent header
sip_utils::addUserAgentHeader(acc->getUserAgentName(), tdata);
result = pjsip_inv_send_msg(inviteSession_.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(inviteSession_.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;
}
int
SIPCall::SIPSessionReinvite()
{
auto mediaList = getMediaAttributeList();
return SIPSessionReinvite(mediaList);
}
void
SIPCall::sendSIPInfo(std::string_view body, std::string_view subtype)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (not inviteSession_ or not inviteSession_->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(inviteSession_->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(inviteSession_->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::setInviteSession(pjsip_inv_session* inviteSession)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (inviteSession == nullptr and inviteSession_) {
JAMI_DBG("[call:%s] Delete current invite session", getCallId().c_str());
} else if (inviteSession != nullptr) {
// NOTE: The first reference of the invite session is owned by pjsip. If
// that counter goes down to zero the invite will be destroyed, and the
// unique_ptr will point freed datas. To avoid this, we increment the
// ref counter and let our unique_ptr share the ownership of the session
// with pjsip.
if (PJ_SUCCESS != pjsip_inv_add_ref(inviteSession)) {
JAMI_WARN("[call:%s] trying to set invalid invite session [%p]",
getCallId().c_str(),
inviteSession);
inviteSession_.reset(nullptr);
return;
}
JAMI_DBG("[call:%s] Set new invite session [%p]", getCallId().c_str(), inviteSession);
} else {
// Nothing to do.
return;
}
inviteSession_.reset(inviteSession);
}
void
SIPCall::terminateSipSession(int status)
{
JAMI_DBG("[call:%s] Terminate SIP session", getCallId().c_str());
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (inviteSession_ and inviteSession_->state != PJSIP_INV_STATE_DISCONNECTED) {
pjsip_tx_data* tdata = nullptr;
auto ret = pjsip_inv_end_session(inviteSession_.get(), status, nullptr, &tdata);
if (ret == PJ_SUCCESS) {
if (tdata) {
auto account = getSIPAccount();
if (account) {
sip_utils::addContactHeader(account->getContactHeader(
transport_ ? transport_->get() : nullptr),
tdata);
// Add user-agent header
sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
} else {
JAMI_ERR("No account detected");
std::ostringstream msg;
msg << "[call:" << getCallId().c_str() << "] "
<< "The account owning this call is invalid";
throw std::runtime_error(msg.str());
}
ret = pjsip_inv_send_msg(inviteSession_.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(),
inviteSession_.get(),
sip_utils::sip_strerror(ret).c_str());
}
setInviteSession();
}
void
SIPCall::answer()
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return;
}
if (not inviteSession_)
throw VoipLinkException("[call:" + getCallId()
+ "] answer: no invite session for this call");
if (!inviteSession_->neg) {
JAMI_WARN("[call:%s] Negotiator is NULL, we've received an INVITE without an SDP",
getCallId().c_str());
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
}
setContactHeader(account->getContactHeader(transport_ ? transport_->get() : nullptr));
pjsip_tx_data* tdata;
if (!inviteSession_->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(inviteSession_.get(),
PJSIP_SC_OK,
NULL,
!inviteSession_->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::addUserAgentHeader(account->getUserAgentName(), tdata);
if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
setInviteSession();
throw std::runtime_error("Could not send invite request answer (200 OK)");
}
setState(CallState::ACTIVE, ConnectionState::CONNECTED);
}
void
SIPCall::answer(const std::vector<DRing::MediaMap>& mediaList)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
auto account = getSIPAccount();
if (not account) {
JAMI_ERR("No account detected");
return;
}
auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
if (mediaAttrList.empty()) {
JAMI_DBG("[call:%s] Media list must not be empty!", getCallId().c_str());
return;
}
if (not inviteSession_)
JAMI_DBG("[call:%s] No invite session for this call", getCallId().c_str());
JAMI_DBG("[call:%s] Answering incoming call with %lu media:",
getCallId().c_str(),
mediaAttrList.size());
if (mediaAttrList.size() != rtpStreams_.size()) {
JAMI_ERR("[call:%s] Media list size %lu in answer does not match. Expected %lu",
getCallId().c_str(),
mediaAttrList.size(),
rtpStreams_.size());
return;
}
for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
auto const& mediaAttr = mediaAttrList.at(idx);
JAMI_DBG("[call:%s] Media @%lu: %s",
getCallId().c_str(),
idx,
mediaAttr.toString(true).c_str());
}
// Apply the media attributes provided by the user.
for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
updateMediaStream(mediaAttrList[idx], idx);
}
if (not inviteSession_)
throw VoipLinkException("[call:" + getCallId()
+ "] answer: no invite session for this call");
if (not inviteSession_->neg) {
// We are answering to an INVITE that did not include a media offer (SDP).
// The SIP specification (RFCs 3261/6337) requires that if a UA wishes to
// proceed with the call, it must provide a media offer (SDP) if the initial
// INVITE did not offer one. In this case, the SDP offer will be included in
// the SIP OK (200) answer. The peer UA will then include its SDP answer in
// the SIP ACK message.
// TODO. This code should be unified with the code used by accounts to create
// SDP offers.
JAMI_WARN("[call:%s] No negotiator session, peer sent an empty INVITE (without SDP)",
getCallId().c_str());
Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
generateMediaPorts();
// Setup and create ICE offer
if (isIceEnabled()) {
sdp_->clearIce();
auto opts = account->getIceOptions();
auto publicAddr = account->getPublishedIpAddress();
if (publicAddr) {
opts.accountPublicAddr = publicAddr;
if (auto interfaceAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
publicAddr.getFamily())) {
opts.accountLocalAddr = interfaceAddr;
createIceMediaTransport();
if (initIceMediaTransport(true, std::move(opts)))
addLocalIceAttributes();
} else {
JAMI_WARN("[call:%s] Cant init ICE transport, missing local address",
getCallId().c_str());
}
} else {
JAMI_WARN("[call:%s] Cant init ICE transport, missing public address",
getCallId().c_str());
}
}
}
setContactHeader(account->getContactHeader(transport_ ? transport_->get() : nullptr));
if (!inviteSession_->last_answer)
throw std::runtime_error("Should only be called for initial answer");
// Answer with an SDP offer if the initial invite was empty,
// otherwise, set the local_sdp session to null to use the
// current SDP session.
pjsip_tx_data* tdata;
if (pjsip_inv_answer(inviteSession_.get(),
PJSIP_SC_OK,
NULL,
not inviteSession_->neg ? sdp_->getLocalSdpSession() : NULL,
&tdata)
!= PJ_SUCCESS)
throw std::runtime_error("Could not init invite request answer (200 OK)");
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::addUserAgentHeader(account->getUserAgentName(), tdata);
if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
setInviteSession();
throw std::runtime_error("Could not send invite request answer (200 OK)");
}
setState(CallState::ACTIVE, ConnectionState::CONNECTED);
}
void
SIPCall::answerMediaChangeRequest(const std::vector<DRing::MediaMap>& mediaList)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
auto account = getSIPAccount();
if (not account) {
JAMI_ERR("[call:%s] No account detected", getCallId().c_str());
return;
}
auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
if (mediaAttrList.empty()) {
JAMI_DBG("[call:%s] Media list size is empty. Ignoring the media change request",
getCallId().c_str());
return;
}
if (not sdp_) {
JAMI_ERR("[call:%s] No valid SDP session", getCallId().c_str());
return;
}
JAMI_DBG("[call:%s] Current media", getCallId().c_str());
unsigned idx = 0;
for (auto const& rtp : rtpStreams_) {
JAMI_DBG("[call:%s] Media @%u: %s",
getCallId().c_str(),
idx++,
rtp.mediaAttribute_->toString(true).c_str());
}
JAMI_DBG("[call:%s] Answering to media change request with new media", getCallId().c_str());
idx = 0;
for (auto const& newMediaAttr : mediaAttrList) {
JAMI_DBG("[call:%s] Media @%u: %s",
getCallId().c_str(),
idx++,
newMediaAttr.toString(true).c_str());
}
updateAllMediaStreams(mediaAttrList);
if (not sdp_->processIncomingOffer(mediaAttrList)) {
JAMI_WARN("[call:%s] Could not process the new offer, ignoring", getCallId().c_str());
return;
}
if (not sdp_->getRemoteSdpSession()) {
JAMI_ERR("[call:%s] No valid remote SDP session", getCallId().c_str());
return;
}
if (isIceEnabled())
setupIceResponse();
if (not sdp_->startNegotiation()) {
JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
getCallId().c_str());
return;
}
if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
getCallId().c_str());
return;
}
pjsip_tx_data* tdata;
if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, NULL, &tdata) != PJ_SUCCESS) {
JAMI_ERR("[call:%s] Could not init answer to a re-invite request", getCallId().c_str());
return;
}
if (contactHeader_.slen) {
sip_utils::addContactHeader(contactHeader_, tdata);
}
// Add user-agent header
sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
JAMI_ERR("[call:%s] Could not send answer to a re-invite request", getCallId().c_str());
setInviteSession();
return;
}
JAMI_DBG("[call:%s] Successfully answered the media change request", getCallId().c_str());
}
void
SIPCall::hangup(int reason)
{
std::lock_guard<std::recursive_mutex> lk {callMutex_};
pendingRecord_ = false;
if (inviteSession_ and inviteSession_->dlg) {
pjsip_route_hdr* route = inviteSession_->dlg->route_set.next;
while (route and route != &inviteSession_->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;
}
int status = PJSIP_SC_OK;
if (reason)
status = reason;
else if (inviteSession_->state <= PJSIP_INV_STATE_EARLY
and inviteSession_->role != PJSIP_ROLE_UAC)
status = PJSIP_SC_CALL_TSX_DOES_NOT_EXIST;
else if (inviteSession_->state >= PJSIP_INV_STATE_DISCONNECTED)
status = PJSIP_SC_DECLINE;
// 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 !inviteSession_)
return;
stopAllMedia();
// Notify the peer
terminateSipSession(PJSIP_SC_BUSY_HERE);
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->inviteSession_)
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 inviteSession_ or not inviteSession_->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(inviteSession_->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)
{
auto toCall = Manager::instance().callFactory.getCall<SIPCall>(to);
if (!toCall)
return false;
if (not toCall->inviteSession_ or not toCall->inviteSession_->dlg)
return false;
pjsip_dialog* target_dlg = toCall->inviteSession_->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 {
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& source)
{
JAMI_DBG("[call:%s] Set selected source to %s", getCallId().c_str(), source.c_str());
for (auto const& stream : rtpStreams_) {
auto mediaAttr = stream.mediaAttribute_;
mediaAttr->sourceUri_ = source;
}
// Check if the call is being recorded in order to continue
// ... the recording after the switch
bool isRec = Call::isRecording();
if (isWaitingForIceAndMedia_) {
remainingRequest_ = Request::SwitchInput;
} else {
if (SIPSessionReinvite() == PJ_SUCCESS) {
isWaitingForIceAndMedia_ = true;
}
}
if (isRec) {
readyToRecord_ = false;
resetMediaReady();
pendingRecord_ = true;
}
}
void
SIPCall::peerHungup()
{
pendingRecord_ = false;
// Stop all RTP streams
stopAllMedia();
if (inviteSession_)
terminateSipSession(PJSIP_SC_NOT_FOUND);
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 %d", rotation);
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 (inviteSession_) {
try {
im::sendSipMessage(inviteSession_.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()
{
#ifdef ENABLE_PLUGIN
jami::Manager::instance().getJamiPluginManager().getCallServicesManager().clearCallHandlerMaps(
getCallId());
#endif
std::lock_guard<std::recursive_mutex> lk {callMutex_};
JAMI_DBG("[call:%s] removeCall()", getCallId().c_str());
if (sdp_) {
sdp_->setActiveLocalSdpSession(nullptr);
sdp_->setActiveRemoteSdpSession(nullptr);
}
Call::removeCall();
{
std::lock_guard<std::mutex> lk(transportMtx_);
resetTransport(std::move(mediaTransport_));
}
setInviteSession();
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");
if (auto const& videoRtp = sthis->getVideoRtp()) {
videoRtp->forceKeyFrame();
}
}
});
#endif
}
bool
SIPCall::isIceEnabled() const
{
return enableIce_;
}
void
SIPCall::setPeerUaVersion(std::string_view ua)
{
if (peerUserAgent_ == ua or ua.empty()) {
// Silently ignore if it did not change or empty.
return;
}
if (peerUserAgent_.empty()) {
JAMI_DBG("[call:%s] Set peer's User-Agent to [%.*s]",
getCallId().c_str(),
(int) ua.size(),
ua.data());
} else if (not peerUserAgent_.empty()) {
// Unlikely, but should be handled since we dont have control over the peer.
// Even if it's unexpected, we still try to parse the UA version.
JAMI_WARN("[call:%s] Peer's User-Agent unexpectedly changed from [%s] to [%.*s]",
getCallId().c_str(),
peerUserAgent_.c_str(),
(int) ua.size(),
ua.data());
}
peerUserAgent_ = ua;
// User-agent parsing
constexpr std::string_view PACK_NAME(PACKAGE_NAME " ");
auto pos = ua.find(PACK_NAME);
if (pos == std::string_view::npos) {
// Must have the expected package name.
JAMI_WARN("Could not find the expected package name in peer's User-Agent");
return;
}
ua = ua.substr(pos + PACK_NAME.length());
std::string_view version;
// Unstable (un-released) versions has a hiphen + commit Id after
// the version number. Find the commit Id if any, and ignore it.
pos = ua.find('-');
if (pos != std::string_view::npos) {
// Get the version and ignore the commit ID.
version = ua.substr(0, pos);
} else {
// Extract the version number.
pos = ua.find(' ');
if (pos != std::string_view::npos) {
version = ua.substr(0, pos);
}
}
if (version.empty()) {
JAMI_DBG("[call:%s] Could not parse peer's version", getCallId().c_str());
return;
}
auto peerVersion = split_string_to_unsigned(version, '.');
if (peerVersion.size() > 4u) {
JAMI_WARN("[call:%s] Could not parse peer's version", getCallId().c_str());
return;
}
// Check if peer's version is at least 10.0.2 to enable multi-stream.
peerSupportMultiStream_ = Account::meetMinimumRequiredVersion(peerVersion,
MULTISTREAM_REQUIRED_VERSION);
if (not peerSupportMultiStream_) {
JAMI_DBG(
"Peer's version [%.*s] does not support multi-stream. Min required version: [%.*s]",
(int) version.size(),
version.data(),
(int) MULTISTREAM_REQUIRED_VERSION_STR.size(),
MULTISTREAM_REQUIRED_VERSION_STR.data());
}
}
void
SIPCall::onPeerRinging()
{
JAMI_DBG("[call:%s] Peer ringing", getCallId().c_str());
setState(ConnectionState::RINGING);
}
void
SIPCall::addLocalIceAttributes()
{
if (not isIceEnabled())
return;
auto mediaTransport = getIceMedia();
if (not mediaTransport) {
JAMI_ERR("[call:%s] Invalid ICE instance", getCallId().c_str());
return;
}
auto start = std::chrono::steady_clock::now();
if (not mediaTransport->isInitialized()) {
JAMI_DBG("[call:%s] Waiting for ICE initialization", getCallId().c_str());
// we need an initialized ICE to progress further
if (mediaTransport->waitForInitialization(DEFAULT_ICE_INIT_TIMEOUT) <= 0) {
JAMI_ERR("[call:%s] ICE initialization timed out", getCallId().c_str());
return;
}
// ICE initialization may take longer than usual in some cases,
// for instance when TURN servers do not respond in time (DNS
// resolution or other issues).
auto duration = std::chrono::steady_clock::now() - start;
if (duration > EXPECTED_ICE_INIT_MAX_TIME) {
JAMI_WARN("[call:%s] ICE initialization time was unexpectedly high (%ld ms)",
getCallId().c_str(),
std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
}
}
// Check the state of ICE instance, the initialization may have failed.
if (not mediaTransport->isInitialized()) {
JAMI_ERR("[call:%s] ICE session is not initialized", getCallId().c_str());
return;
}
// Check the state, the call might have been canceled while waiting.
// for initialization.
if (getState() == Call::CallState::OVER) {
JAMI_WARN("[call:%s] The call was terminated while waiting for ICE initialization",
getCallId().c_str());
return;
}
auto account = getSIPAccount();
if (not account) {
JAMI_ERR("No account detected");
return;
}
if (not sdp_) {
JAMI_ERR("No sdp detected");
return;
}
JAMI_DBG("[call:%s] Add local attributes for ICE instance [%p]",
getCallId().c_str(),
mediaTransport.get());
sdp_->addIceAttributes(mediaTransport->getLocalAttributes());
if (account->isIceCompIdRfc5245Compliant()) {
unsigned streamIdx = 0;
for (auto const& stream : rtpStreams_) {
if (not stream.mediaAttribute_->enabled_) {
// Dont add ICE candidates if the media is disabled
JAMI_DBG("[call:%s] media [%s] @ %u is disabled, dont add local candidates",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
streamIdx);
continue;
}
JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
streamIdx);
// RTP
sdp_->addIceCandidates(streamIdx,
mediaTransport->getLocalCandidates(streamIdx, ICE_COMP_ID_RTP));
// RTCP if it has its own port
if (not rtcpMuxEnabled_) {
sdp_->addIceCandidates(streamIdx,
mediaTransport->getLocalCandidates(streamIdx,
ICE_COMP_ID_RTP + 1));
}
streamIdx++;
}
} else {
unsigned idx = 0;
unsigned compId = 1;
for (auto const& stream : rtpStreams_) {
if (not stream.mediaAttribute_->enabled_) {
// Skipping local ICE candidates if the media is disabled
continue;
}
JAMI_DBG("[call:%s] add ICE local candidates for media [%s] @ %u",
getCallId().c_str(),
stream.mediaAttribute_->toString().c_str(),
idx);
// RTP
sdp_->addIceCandidates(idx, mediaTransport->getLocalCandidates(compId));
compId++;
// RTCP if it has its own port
if (not rtcpMuxEnabled_) {
sdp_->addIceCandidates(idx, mediaTransport->getLocalCandidates(compId));
compId++;
}
idx++;
}
}
}
std::vector<IceCandidate>
SIPCall::getAllRemoteCandidates(IceTransport& transport) const
{
std::vector<IceCandidate> rem_candidates;
for (unsigned mediaIdx = 0; mediaIdx < static_cast<unsigned>(rtpStreams_.size()); mediaIdx++) {
IceCandidate cand;
for (auto& line : sdp_->getIceCandidates(mediaIdx)) {
if (transport.parseIceAttributeLine(mediaIdx, line, cand)) {
JAMI_DBG("[call:%s] Add remote ICE candidate: %s",
getCallId().c_str(),
line.c_str());
rem_candidates.emplace_back(std::move(cand));
}
}
}
return rem_candidates;
}
std::shared_ptr<AccountCodecInfo>
SIPCall::getVideoCodec() const
{
#ifdef ENABLE_VIDEO
if (auto const& videoRtp = getVideoRtp())
return videoRtp->getCodec();
#endif
return {};
}
std::shared_ptr<AccountCodecInfo>
SIPCall::getAudioCodec() const
{
if (auto const& audioRtp = getAudioRtp())
return audioRtp->getCodec();
return {};
}
void
SIPCall::addMediaStream(const MediaAttribute& mediaAttr)
{
// Create and add the media stream with the provided attribute.
// Do not create the RTP sessions yet.
RtpStream stream;
stream.mediaAttribute_ = std::make_shared<MediaAttribute>(mediaAttr);
// Set default media source if empty. Kept for backward compatibility.
#ifdef ENABLE_VIDEO
if (stream.mediaAttribute_->sourceUri_.empty()) {
stream.mediaAttribute_->sourceUri_
= Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice();
}
#endif
if (stream.mediaAttribute_->sourceType_ == MediaSourceType::NONE) {
stream.mediaAttribute_->sourceType_ = MediaSourceType::CAPTURE_DEVICE;
}
rtpStreams_.emplace_back(std::move(stream));
}
size_t
SIPCall::initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
{
for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
auto const& mediaAttr = mediaAttrList.at(idx);
if (mediaAttr.type_ != MEDIA_AUDIO && mediaAttr.type_ != MEDIA_VIDEO) {
JAMI_ERR("[call:%s] Unexpected media type %u", getCallId().c_str(), mediaAttr.type_);
assert(false);
}
addMediaStream(mediaAttr);
auto& stream = rtpStreams_.back();
createRtpSession(stream);
JAMI_DBG("[call:%s] Added media @%lu: %s",
getCallId().c_str(),
idx,
stream.mediaAttribute_->toString(true).c_str());
}
JAMI_DBG("[call:%s] Created %lu Media streams", getCallId().c_str(), rtpStreams_.size());
return rtpStreams_.size();
}
bool
SIPCall::hasVideo() const
{
#ifdef ENABLE_VIDEO
std::function<bool(const RtpStream& stream)> videoCheck = [](auto const& stream) {
return stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO;
};
const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), videoCheck);
return iter != rtpStreams_.end();
#else
return false;
#endif
}
bool
SIPCall::isCaptureDeviceMuted(const MediaType& mediaType) const
{
// Return true only if all media of type 'mediaType' that use capture devices
// source, are muted.
std::function<bool(const RtpStream& stream)> mutedCheck = [&mediaType](auto const& stream) {
return (stream.mediaAttribute_->type_ == mediaType
and stream.mediaAttribute_->sourceType_ == MediaSourceType::CAPTURE_DEVICE
and not stream.mediaAttribute_->muted_);
};
const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck);
return iter == rtpStreams_.end();
}
void
SIPCall::updateNegotiatedMedia()
{
JAMI_DBG("[call:%s] updating negotiated media", getCallId().c_str());
if (not transport_ or not sdp_) {
JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
return;
}
auto slots = sdp_->getMediaSlots();
bool peer_holding {true};
int streamIdx = -1;
for (const auto& slot : slots) {
streamIdx++;
const auto& local = slot.first;
const auto& remote = slot.second;
// Skip disabled media
if (not local.enabled) {
JAMI_DBG("[call:%s] [SDP:slot#%u] The media is disabled, skipping",
getCallId().c_str(),
streamIdx);
continue;
}
if (static_cast<size_t>(streamIdx) >= rtpStreams_.size()) {
throw std::runtime_error("Stream index is out-of-range");
}
auto const& rtpStream = rtpStreams_[streamIdx];
if (not rtpStream.mediaAttribute_) {
throw std::runtime_error("Missing media attribute");
}
// To enable a media, it must be enabled on both sides.
rtpStream.mediaAttribute_->enabled_ = local.enabled and remote.enabled;
if (not rtpStream.rtpSession_)
throw std::runtime_error("Must have a valid RTP Session");
if (local.type != MEDIA_AUDIO && local.type != MEDIA_VIDEO) {
JAMI_ERR("[call:%s] Unexpected media type %u", getCallId().c_str(), local.type);
throw std::runtime_error("Invalid media attribute");
}
if (local.type != remote.type) {
JAMI_ERR("[call:%s] [SDP:slot#%u] Inconsistent media type between local and remote",
getCallId().c_str(),
streamIdx);
continue;
}
if (local.enabled and not local.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing local codec", getCallId().c_str(), streamIdx);
continue;
}
if (remote.enabled and not remote.codec) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Missing remote codec",
getCallId().c_str(),
streamIdx);
continue;
}
if (isSrtpEnabled() and local.enabled and not local.crypto) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no local crypto attributes. "
"Ignoring the media",
getCallId().c_str(),
streamIdx);
continue;
}
if (isSrtpEnabled() and remote.enabled and not remote.crypto) {
JAMI_WARN("[call:%s] [SDP:slot#%u] Secure mode but no crypto remote attributes. "
"Ignoring the media",
getCallId().c_str(),
streamIdx);
continue;
}
// Aggregate holding info over all remote streams
peer_holding &= remote.onHold;
configureRtpSession(rtpStream.rtpSession_, rtpStream.mediaAttribute_, local, remote);
}
if (not isSubcall() and peerHolding_ != peer_holding) {
peerHolding_ = peer_holding;
emitSignal<DRing::CallSignal::PeerHold>(getCallId(), peerHolding_);
}
// Notify using the parent Id if it's a subcall.
auto callId = isSubcall() ? parent_->getCallId() : getCallId();
emitSignal<DRing::CallSignal::MediaNegotiationStatus>(
callId,
DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS,
MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()));
}
void
SIPCall::startAllMedia()
{
JAMI_DBG("[call:%s] starting the media", getCallId().c_str());
if (not transport_ or not sdp_) {
JAMI_ERR("[call:%s] the call is in invalid state", getCallId().c_str());
return;
}
if (isSrtpEnabled() && not transport_->isSecure()) {
JAMI_WARN("[call:%s] Crypto (SRTP) is negotiated over an insecure signaling transport",
getCallId().c_str());
}
// reset
readyToRecord_ = false;
resetMediaReady();
#ifdef ENABLE_VIDEO
bool isVideoEnabled = false;
#endif
int currentCompId = 1;
for (auto iter = rtpStreams_.begin(); iter != rtpStreams_.end(); iter++) {
if (not iter->mediaAttribute_) {
throw std::runtime_error("Missing media attribute");
}
#ifdef ENABLE_VIDEO
if (iter->mediaAttribute_->type_ == MEDIA_VIDEO)
isVideoEnabled |= iter->mediaAttribute_->enabled_;
#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()) {
// Create sockets for RTP and RTCP, and start the session.
auto iceRtpSocket = newIceSocket(currentCompId++);
std::unique_ptr<IceSocket> iceRtcpSocket;
if (not rtcpMuxEnabled_) {
iceRtcpSocket = newIceSocket(currentCompId++);
}
iter->rtpSession_->start(std::move(iceRtpSocket), std::move(iceRtcpSocket));
} else {
iter->rtpSession_->start(nullptr, nullptr);
}
}
}
#ifdef ENABLE_VIDEO
// TODO. Move this elsewhere (when adding participant to conf?)
if (!isVideoEnabled && !getConfId().empty()) {
auto conference = Manager::instance().getConferenceFromID(getConfId());
conference->attachVideo(getReceiveVideoFrameActiveWriter().get(), getCallId());
}
#endif
// 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());
auto const& audioRtp = getAudioRtp();
if (audioRtp)
audioRtp->restartSender();
#ifdef ENABLE_VIDEO
if (hasVideo()) {
auto const& videoRtp = getVideoRtp();
if (videoRtp)
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
}
auto const& audioRtp = getAudioRtp();
if (audioRtp)
audioRtp->stop();
#ifdef ENABLE_VIDEO
auto const& videoRtp = getVideoRtp();
{
std::lock_guard<std::mutex> lk(sinksMtx_);
for (auto it = callSinksMap_.begin(); it != callSinksMap_.end();) {
auto& videoReceive = videoRtp->getVideoReceive();
if (videoReceive) {
videoReceive->detach(it->second.get());
}
it->second->stop();
it = callSinksMap_.erase(it);
}
}
if (videoRtp)
videoRtp->stop();
#endif
#ifdef ENABLE_PLUGIN
{
clearCallAVStreams();
std::lock_guard<std::mutex> lk(avStreamsMtx_);
Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject(
getCallId());
}
#endif
}
void
SIPCall::muteMedia(const std::string& mediaType, bool mute)
{
auto type = MediaAttribute::stringToMediaType(mediaType);
if (type == MediaType::MEDIA_AUDIO) {
JAMI_WARN("[call:%s] %s all audio medias",
getCallId().c_str(),
mute ? "muting " : "un-muting ");
} else if (type == MediaType::MEDIA_VIDEO) {
JAMI_WARN("[call:%s] %s all video medias",
getCallId().c_str(),
mute ? "muting" : "un-muting");
} else {
JAMI_ERR("[call:%s] invalid media type %s", getCallId().c_str(), mediaType.c_str());
assert(false);
}
// Get the current media attributes.
auto mediaList = getMediaAttributeList();
// Mute/Un-mute all medias with matching type.
for (auto& mediaAttr : mediaList) {
if (mediaAttr.type_ == type) {
mediaAttr.muted_ = mute;
}
}
// Apply
requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList));
}
void
SIPCall::updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx)
{
assert(streamIdx < rtpStreams_.size());
auto const& rtpStream = rtpStreams_[streamIdx];
assert(rtpStream.rtpSession_);
auto const& mediaAttr = rtpStream.mediaAttribute_;
assert(mediaAttr);
bool notify = false;
if (newMediaAttr.muted_ == mediaAttr->muted_) {
// Nothing to do. Already in the desired state.
JAMI_DBG("[call:%s] [%s] already %s",
getCallId().c_str(),
mediaAttr->label_.c_str(),
mediaAttr->muted_ ? "muted " : "un-muted ");
} else {
notify = true;
}
// Update
mediaAttr->muted_ = newMediaAttr.muted_;
// Only update source and type if actually set.
if (not newMediaAttr.sourceUri_.empty())
mediaAttr->sourceUri_ = newMediaAttr.sourceUri_;
if (newMediaAttr.sourceType_ != MediaSourceType::NONE)
mediaAttr->sourceType_ = newMediaAttr.sourceType_;
JAMI_DBG("[call:%s] %s [%s]",
getCallId().c_str(),
mediaAttr->muted_ ? "muting" : "un-muting",
mediaAttr->label_.c_str());
if (notify and mediaAttr->type_ == MediaType::MEDIA_AUDIO) {
rtpStream.rtpSession_->setMuted(mediaAttr->muted_);
setMute(mediaAttr->muted_);
if (not isSubcall())
emitSignal<DRing::CallSignal::AudioMuted>(getCallId(), mediaAttr->muted_);
return;
}
#ifdef ENABLE_VIDEO
if (notify and mediaAttr->type_ == MediaType::MEDIA_VIDEO and not isSubcall()) {
emitSignal<DRing::CallSignal::VideoMuted>(getCallId(), mediaAttr->muted_);
}
#endif
}
void
SIPCall::updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
{
JAMI_DBG("[call:%s] New local medias", getCallId().c_str());
unsigned idx = 0;
for (auto const& newMediaAttr : mediaAttrList) {
JAMI_DBG("[call:%s] Media @%u: %s",
getCallId().c_str(),
idx++,
newMediaAttr.toString(true).c_str());
}
JAMI_DBG("[call:%s] Updating local media streams", getCallId().c_str());
for (auto const& newAttr : mediaAttrList) {
auto streamIdx = findRtpStreamIndex(newAttr.label_);
if (streamIdx == rtpStreams_.size()) {
// Media does not exist, add a new one.
addMediaStream(newAttr);
auto& stream = rtpStreams_.back();
createRtpSession(stream);
JAMI_DBG("[call:%s] Added a new media stream [%s] @ index %lu",
getCallId().c_str(),
stream.mediaAttribute_->label_.c_str(),
streamIdx);
} else {
updateMediaStream(newAttr, streamIdx);
}
}
}
bool
SIPCall::isReinviteRequired(const std::vector<MediaAttribute>& mediaAttrList)
{
if (mediaAttrList.size() != rtpStreams_.size())
return true;
for (auto const& newAttr : mediaAttrList) {
auto streamIdx = findRtpStreamIndex(newAttr.label_);
if (streamIdx == rtpStreams_.size()) {
// Always needs a reinvite when a new media is added.
return true;
}
#ifdef ENABLE_VIDEO
if (newAttr.type_ == MediaType::MEDIA_VIDEO) {
assert(rtpStreams_[streamIdx].mediaAttribute_);
// Changes in video attributes always trigger a re-invite.
return newAttr.muted_ != rtpStreams_[streamIdx].mediaAttribute_->muted_;
}
#endif
}
return false;
}
bool
SIPCall::requestMediaChange(const std::vector<DRing::MediaMap>& mediaList)
{
auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
// If the peer does not support multi-stream and the size of the new
// media list is different from the current media list, the media
// change request will be ignored.
if (not peerSupportMultiStream_ and rtpStreams_.size() != mediaAttrList.size()) {
JAMI_WARN("[call:%s] Peer does not support multi-stream. Media change request ignored",
getCallId().c_str());
return false;
}
JAMI_DBG("[call:%s] Requesting media change. List of new media:", getCallId().c_str());
unsigned idx = 0;
for (auto const& newMediaAttr : mediaAttrList) {
JAMI_DBG("[call:%s] Media @%u: %s",
getCallId().c_str(),
idx++,
newMediaAttr.toString(true).c_str());
}
auto needReinvite = isReinviteRequired(mediaAttrList);
updateAllMediaStreams(mediaAttrList);
if (needReinvite) {
JAMI_DBG("[call:%s] Media change requires a new negotiation (re-invite)",
getCallId().c_str());
requestReinvite();
}
return true;
}
std::vector<MediaAttribute>
SIPCall::getMediaAttributeList() const
{
std::vector<MediaAttribute> mediaList;
mediaList.reserve(rtpStreams_.size());
for (auto const& stream : rtpStreams_) {
mediaList.emplace_back(*stream.mediaAttribute_);
}
return mediaList;
}
/// \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 asynchronously when
/// the transport is negotiated.
void
SIPCall::onMediaNegotiationComplete()
{
JAMI_WARN("[call:%s] Media negotiation complete", 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_};
JAMI_WARN("[call:%s] media changed", this_->getCallId().c_str());
// The call is already ended, so we don't need to restart medias
if (not this_->inviteSession_
or this_->inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED
or not this_->sdp_) {
return;
}
bool hasIce = this_->isIceEnabled();
if (hasIce) {
// If ICE is not used, start medias now
auto rem_ice_attrs = this_->sdp_->getIceAttributes();
hasIce = not rem_ice_attrs.ufrag.empty() and not rem_ice_attrs.pwd.empty();
}
if (hasIce) {
if (not this_->isSubcall()) {
// Start ICE checks. Media will be started once ICE checks complete.
this_->startIceMedia();
}
} else {
// No ICE, start media now.
JAMI_WARN("[call:%s] ICE media disabled, using default media ports",
this_->getCallId().c_str());
// Update the negotiated media.
this_->updateNegotiatedMedia();
// Start the media.
this_->stopAllMedia();
this_->startAllMedia();
}
}
});
}
void
SIPCall::startIceMedia()
{
JAMI_DBG("[call:%s] Starting ICE", getCallId().c_str());
auto mediaTransport = getIceMedia();
if (not mediaTransport or mediaTransport->isFailed()) {
JAMI_ERR("[call:%s] Media ICE init failed", getCallId().c_str());
onFailure(EIO);
return;
}
if (mediaTransport->isStarted()) {
// NOTE: for incoming calls, the ice is already there and running
if (mediaTransport->isRunning())
onIceNegoSucceed();
return;
}
if (not mediaTransport->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 mediaTransport->startIce(rem_ice_attrs, getAllRemoteCandidates(*mediaTransport))) {
JAMI_ERR("[call:%s] Media ICE start failed", getCallId().c_str());
onFailure(EIO);
}
}
void
SIPCall::onIceNegoSucceed()
{
JAMI_DBG("[call:%s] ICE negotiation succeeded", getCallId().c_str());
// Check if the call is already ended, so we don't need to restart medias
// This is typically the case in a multi-device context where one device
// can stop a call. So do not start medias
if (not inviteSession_ or inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED or not sdp_) {
JAMI_ERR("[call:%s] ICE negotiation succeeded, but call is in invalid state",
getCallId().c_str());
return;
}
// Update the negotiated media.
updateNegotiatedMedia();
// Nego succeed: move to the new media transport
stopAllMedia();
startAllMedia();
}
pj_status_t
SIPCall::onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdata)
{
JAMI_DBG("[call:%s] Received a re-invite", getCallId().c_str());
pj_status_t res = PJ_SUCCESS;
if (not sdp_) {
JAMI_ERR("SDP session is invalid");
return res;
}
sdp_->clearIce();
auto acc = getSIPAccount();
if (not acc) {
JAMI_ERR("No account detected");
return res;
}
Sdp::printSession(offer, "Remote session (media change request)", SdpDirection::OFFER);
sdp_->setReceivedOffer(offer);
auto const& mediaAttrList = Sdp::getMediaAttributeListFromSdp(offer);
if (mediaAttrList.empty()) {
JAMI_WARN("[call:%s] Media list is empty, ignoring", getCallId().c_str());
return res;
}
if (upnp_) {
openPortsUPnP();
}
pjsip_tx_data* tdata = nullptr;
if (pjsip_inv_initial_answer(inviteSession_.get(), rdata, PJSIP_SC_TRYING, NULL, NULL, &tdata)
!= PJ_SUCCESS) {
JAMI_ERR("Could not create answer TRYING");
return res;
}
// Report the change request.
auto const& remoteMediaList = MediaAttribute::mediaAttributesToMediaMaps(mediaAttrList);
// TODO_MC. Validate this assessment.
// Report re-invites only if the number of media changed, otherwise answer
// using the current local attributes.
if (acc->isMultiStreamEnabled() and remoteMediaList.size() != rtpStreams_.size()) {
Manager::instance().mediaChangeRequested(getCallId(), getAccountId(), remoteMediaList);
} else {
auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
answerMediaChangeRequest(localMediaList);
}
return res;
}
int
SIPCall::onReceiveOffer(const pjmedia_sdp_session* offer, const pjsip_rx_data* rdata)
{
if (!sdp_)
return !PJ_SUCCESS;
sdp_->clearIce();
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return !PJ_SUCCESS;
}
JAMI_DBG("[call:%s] Received a new offer (re-invite)", getCallId().c_str());
sdp_->setReceivedOffer(offer);
// Use current media list.
sdp_->processIncomingOffer(getMediaAttributeList());
if (isIceEnabled() and offer != nullptr) {
setupIceResponse();
}
sdp_->startNegotiation();
pjsip_tx_data* tdata = nullptr;
if (pjsip_inv_initial_answer(inviteSession_.get(),
const_cast<pjsip_rx_data*>(rdata),
PJSIP_SC_OK,
NULL,
NULL,
&tdata)
!= PJ_SUCCESS) {
JAMI_ERR("Could not create initial answer OK");
return !PJ_SUCCESS;
}
// Add user-agent header
sip_utils::addUserAgentHeader(getSIPAccount()->getUserAgentName(), tdata);
if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, sdp_->getLocalSdpSession(), &tdata)
!= PJ_SUCCESS) {
JAMI_ERR("Could not create answer OK");
return !PJ_SUCCESS;
}
// ContactStr must stay in scope as long as tdata
sip_utils::addContactHeader(getSIPAccount()->getContactHeader(getTransport()->get()), tdata);
if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
JAMI_ERR("Could not send msg OK");
return !PJ_SUCCESS;
}
if (upnp_) {
openPortsUPnP();
}
return PJ_SUCCESS;
}
void
SIPCall::onReceiveOfferIn200OK(const pjmedia_sdp_session* offer)
{
if (not rtpStreams_.empty()) {
JAMI_ERR("[call:%s] Unexpected offer in '200 OK' answer", getCallId().c_str());
return;
}
auto acc = getSIPAccount();
if (not acc) {
JAMI_ERR("No account detected");
return;
}
if (not sdp_) {
JAMI_ERR("invalid SDP session");
return;
}
JAMI_DBG("[call:%s] Received an offer in '200 OK' answer", getCallId().c_str());
auto mediaList = Sdp::getMediaAttributeListFromSdp(offer);
// If this method is called, it means we are expecting an offer
// in the 200OK answer.
if (mediaList.empty()) {
JAMI_WARN("[call:%s] Remote media list is empty, ignoring", getCallId().c_str());
return;
}
Sdp::printSession(offer, "Remote session (offer in 200 OK answer)", SdpDirection::OFFER);
sdp_->clearIce();
sdp_->setReceivedOffer(offer);
// If we send an empty offer, video will be accepted only if locally
// enabled by the user.
for (auto& mediaAttr : mediaList) {
if (mediaAttr.type_ == MediaType::MEDIA_VIDEO and not acc->isVideoEnabled()) {
mediaAttr.enabled_ = false;
}
}
initMediaStreams(mediaList);
sdp_->processIncomingOffer(mediaList);
if (upnp_) {
openPortsUPnP();
}
if (isIceEnabled()) {
setupIceResponse();
}
sdp_->startNegotiation();
if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
JAMI_ERR("[call:%s] Could not start media negotiation for a re-invite request",
getCallId().c_str());
}
}
void
SIPCall::openPortsUPnP()
{
if (not sdp_) {
JAMI_ERR("[call:%s] Current SDP instance is invalid", getCallId().c_str());
return;
}
/**
* 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());
// RTP port.
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 acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return {};
}
auto details = Call::getDetails();
details.emplace(DRing::Call::Details::PEER_HOLDING, peerHolding_ ? TRUE_STR : FALSE_STR);
#ifdef ENABLE_VIDEO
for (auto const& stream : rtpStreams_) {
if (stream.mediaAttribute_->type_ != MediaType::MEDIA_VIDEO)
continue;
details.emplace(DRing::Call::Details::VIDEO_SOURCE, stream.mediaAttribute_->sourceUri_);
if (auto const& rtpSession = stream.rtpSession_) {
if (auto codec = rtpSession->getCodec())
details.emplace(DRing::Call::Details::VIDEO_CODEC, codec->systemCodecInfo.name);
else
details.emplace(DRing::Call::Details::VIDEO_CODEC, "");
}
}
#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;
}
void
SIPCall::enterConference(const std::string& confId)
{
#ifdef ENABLE_VIDEO
auto conf = Manager::instance().getConferenceFromID(confId);
if (conf == nullptr) {
JAMI_ERR("Unknown conference [%s]", confId.c_str());
return;
}
auto videoRtp = getVideoRtp();
if (not videoRtp) {
// In conference, we need to have a video RTP session even
// if it's an audio only call
videoRtp = addDummyVideoRtpSession();
if (not videoRtp) {
throw std::runtime_error("Failed to create dummy RTP video session");
}
}
videoRtp->enterConference(conf.get());
#endif
#ifdef ENABLE_PLUGIN
clearCallAVStreams();
#endif
}
void
SIPCall::exitConference()
{
auto const& audioRtp = getAudioRtp();
if (audioRtp && !isCaptureDeviceMuted(MediaType::MEDIA_AUDIO)) {
auto& rbPool = Manager::instance().getRingBufferPool();
rbPool.bindCallID(getCallId(), RingBufferPool::DEFAULT_ID);
rbPool.flush(RingBufferPool::DEFAULT_ID);
}
#ifdef ENABLE_VIDEO
auto const& videoRtp = getVideoRtp();
if (videoRtp)
videoRtp->exitConference();
#endif
#ifdef ENABLE_PLUGIN
createCallAVStreams();
#endif
}
std::shared_ptr<Observable<std::shared_ptr<MediaFrame>>>
SIPCall::getReceiveVideoFrameActiveWriter()
{
#ifdef ENABLE_VIDEO
auto videoRtp = getVideoRtp();
if (videoRtp)
return videoRtp->getReceiveVideoFrameActiveWriter();
#endif
return {};
}
std::shared_ptr<video::VideoRtpSession>
SIPCall::addDummyVideoRtpSession()
{
#ifdef ENABLE_VIDEO
MediaAttribute mediaAttr(MediaType::MEDIA_VIDEO, true, true, false, "", "dummy video session");
addMediaStream(mediaAttr);
auto& stream = rtpStreams_.back();
createRtpSession(stream);
if (stream.rtpSession_) {
return std::dynamic_pointer_cast<video::VideoRtpSession>(stream.rtpSession_);
}
#endif
return {};
}
void
SIPCall::createSinks(const ConfInfo& infos)
{
#ifdef ENABLE_VIDEO
if (!hasVideo())
return;
std::lock_guard<std::mutex> lk(sinksMtx_);
auto videoRtp = getVideoRtp();
auto& videoReceive = videoRtp->getVideoReceive();
if (!videoReceive)
return;
auto id = getConfId().empty() ? getCallId() : getConfId();
Manager::instance().createSinkClients(id,
infos,
std::static_pointer_cast<video::VideoGenerator>(
videoReceive),
callSinksMap_);
#endif
}
std::shared_ptr<AudioRtpSession>
SIPCall::getAudioRtp() const
{
// For the moment, the clients support only one audio stream, so we
// return the first audio stream.
for (auto const& stream : rtpStreams_) {
auto rtp = stream.rtpSession_;
if (rtp->getMediaType() == MediaType::MEDIA_AUDIO) {
return std::dynamic_pointer_cast<AudioRtpSession>(rtp);
}
}
return nullptr;
}
#ifdef ENABLE_VIDEO
std::shared_ptr<video::VideoRtpSession>
SIPCall::getVideoRtp() const
{
for (auto const& stream : rtpStreams_) {
auto rtp = stream.rtpSession_;
if (rtp->getMediaType() == MediaType::MEDIA_VIDEO) {
return std::dynamic_pointer_cast<video::VideoRtpSession>(rtp);
}
}
return nullptr;
}
#endif
std::vector<std::shared_ptr<RtpSession>>
SIPCall::getRtpSessionList() const
{
std::vector<std::shared_ptr<RtpSession>> rtpList;
rtpList.reserve(rtpStreams_.size());
for (auto const& stream : rtpStreams_) {
rtpList.emplace_back(stream.rtpSession_);
}
return rtpList;
}
void
SIPCall::monitor() const
{
if (isSubcall())
return;
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return;
}
JAMI_DBG("- Call %s with %s:", getCallId().c_str(), getPeerNumber().c_str());
JAMI_DBG("\t- Duration: %s", dht::print_duration(getCallDuration()).c_str());
for (const auto& stream : rtpStreams_)
JAMI_DBG("\t- Media: %s", stream.mediaAttribute_->toString(true).c_str());
#ifdef ENABLE_VIDEO
if (auto codec = getVideoCodec())
JAMI_DBG("\t- Video codec: %s", codec->systemCodecInfo.name.c_str());
#endif
if (auto transport = getIceMedia()) {
JAMI_DBG("\t- Medias: %s", transport->link().c_str());
}
}
bool
SIPCall::toggleRecording()
{
pendingRecord_ = true;
if (not readyToRecord_)
return true;
// add streams to recorder before starting the record
if (not Call::isRecording()) {
updateRecState(true);
auto account = getSIPAccount();
if (!account) {
JAMI_ERR("No account detected");
return false;
}
auto title = fmt::format("Conversation at %TIMESTAMP between {} and {}",
account->getUserUri(),
peerUri_);
recorder_->setMetadata(title, ""); // use default description
auto const& audioRtp = getAudioRtp();
if (audioRtp)
audioRtp->initRecorder(recorder_);
#ifdef ENABLE_VIDEO
if (hasVideo()) {
auto const& videoRtp = getVideoRtp();
if (videoRtp)
videoRtp->initRecorder(recorder_);
}
#endif
} else {
updateRecState(false);
deinitRecorder();
}
pendingRecord_ = false;
return Call::toggleRecording();
}
void
SIPCall::deinitRecorder()
{
if (Call::isRecording()) {
auto const& audioRtp = getAudioRtp();
if (audioRtp)
audioRtp->deinitRecorder(recorder_);
#ifdef ENABLE_VIDEO
if (hasVideo()) {
auto const& videoRtp = getVideoRtp();
if (videoRtp)
videoRtp->deinitRecorder(recorder_);
}
#endif
}
}
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);
}
void
SIPCall::createIceMediaTransport()
{
auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
std::lock_guard<std::mutex> lk(transportMtx_);
resetTransport(std::move(mediaTransport_));
mediaTransport_ = iceTransportFactory.createTransport(getCallId().c_str());
if (mediaTransport_) {
JAMI_DBG("[call:%s] Successfully created media ICE transport [ice:%p]",
getCallId().c_str(),
mediaTransport_.get());
} else {
JAMI_ERR("[call:%s] Failed to create media ICE transport", getCallId().c_str());
}
}
bool
SIPCall::initIceMediaTransport(bool master, std::optional<IceTransportOptions> options)
{
auto acc = getSIPAccount();
if (!acc) {
JAMI_ERR("No account detected");
return false;
}
JAMI_DBG("[call:%s] Init media ICE transport", getCallId().c_str());
auto mediaTransport = getIceMedia();
if (not mediaTransport) {
JAMI_ERR("[call:%s] Failed to create media ICE transport", getCallId().c_str());
return false;
}
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 later
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();
}
});
};
iceOptions.master = master;
iceOptions.streamsCount = static_cast<unsigned>(rtpStreams_.size());
// Each RTP stream requires a pair of ICE components (RTP + RTCP).
iceOptions.compCountPerStream = ICE_COMP_COUNT_PER_STREAM;
// Init ICE.
mediaTransport->initIceInstance(iceOptions);
return true;
}
std::vector<std::string>
SIPCall::getLocalIceCandidates(unsigned compId) const
{
std::lock_guard<std::mutex> lk(transportMtx_);
if (not mediaTransport_) {
JAMI_WARN("[call:%s] no media ICE transport", getCallId().c_str());
return {};
}
return mediaTransport_->getLocalCandidates(compId);
}
void
SIPCall::resetTransport(std::shared_ptr<IceTransport>&& transport)
{
// Move the transport to another thread and destroy it there if possible
if (transport) {
dht::ThreadPool::io().run(
[transport = std::move(transport)]() mutable { transport.reset(); });
}
}
void
SIPCall::merge(Call& call)
{
JAMI_DBG("[call:%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};
inviteSession_ = std::move(subcall.inviteSession_);
if (inviteSession_)
inviteSession_->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_;
peerUserAgent_ = subcall.peerUserAgent_;
peerSupportMultiStream_ = subcall.peerSupportMultiStream_;
Call::merge(subcall);
if (isIceEnabled())
startIceMedia();
}
bool
SIPCall::remoteHasValidIceAttributes()
{
if (not sdp_) {
throw std::runtime_error("Must have a valid SDP Session");
}
auto rem_ice_attrs = sdp_->getIceAttributes();
if (rem_ice_attrs.ufrag.empty()) {
JAMI_WARN("[call:%s] Missing ICE username fragment attribute in remote SDP",
getCallId().c_str());
return false;
}
if (rem_ice_attrs.pwd.empty()) {
JAMI_WARN("[call:%s] Missing ICE password attribute in remote SDP", getCallId().c_str());
return false;
}
return true;
}
void
SIPCall::setIceMedia(std::shared_ptr<IceTransport> ice)
{
JAMI_DBG("[call:%s] Setting ICE session [%p]", getCallId().c_str(), ice.get());
std::lock_guard<std::mutex> lk(transportMtx_);
if (not isSubcall()) {
JAMI_ERR("[call:%s] The call is expected to be a sub-call", getCallId().c_str());
}
mediaTransport_ = std::move(ice);
}
void
SIPCall::setupIceResponse()
{
JAMI_DBG("[call:%s] Setup ICE response", getCallId().c_str());
auto account = getSIPAccount();
if (not account) {
JAMI_ERR("No account detected");
}
if (not remoteHasValidIceAttributes()) {
// If ICE attributes are not present, skip the ICE initialization
// step (most likely ICE is not used).
JAMI_ERR("[call:%s] no ICE data in remote SDP", getCallId().c_str());
return;
}
auto opt = account->getIceOptions();
// Try to use the discovered public address. If not available,
// fallback on local address.
opt.accountPublicAddr = account->getPublishedIpAddress();
if (opt.accountLocalAddr) {
opt.accountLocalAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
opt.accountPublicAddr.getFamily());
} else {
// Just set the local address for both, most likely the account is not
// registered.
opt.accountLocalAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(), AF_INET);
opt.accountPublicAddr = opt.accountLocalAddr;
}
if (not opt.accountLocalAddr) {
JAMI_ERR("[call:%s] No local address, ICE can't be initialized", getCallId().c_str());
onFailure(EIO);
return;
}
createIceMediaTransport();
if (not initIceMediaTransport(false, opt)) {
JAMI_ERR("[call:%s] ICE initialization failed", getCallId().c_str());
// 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)
addLocalIceAttributes();
}
bool
SIPCall::isIceRunning() const
{
std::lock_guard<std::mutex> lk(transportMtx_);
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, bool isRemote)
{
std::lock_guard<std::mutex> lk {setupSuccessMutex_};
if (type == MEDIA_AUDIO) {
if (isRemote)
mediaReady_.at("a:remote") = true;
else
mediaReady_.at("a:local") = true;
} else {
if (isRemote)
mediaReady_.at("v:remote") = true;
else
mediaReady_.at("v:local") = true;
}
if (mediaReady_.at("a:local") and mediaReady_.at("a:remote") and mediaReady_.at("v:remote")) {
if (Manager::instance().videoPreferences.getRecordPreview() or mediaReady_.at("v:local"))
readyToRecord_ = true;
}
if (pendingRecord_ && readyToRecord_)
toggleRecording();
}
void
SIPCall::peerRecording(bool state)
{
const std::string& id = getConfId().empty() ? getCallId() : getConfId();
if (state) {
JAMI_WARN("Peer is recording");
emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), true);
} else {
JAMI_WARN("Peer stopped recording");
emitSignal<DRing::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), false);
}
peerRecording_ = state;
}
void
SIPCall::peerMuted(bool muted)
{
if (muted) {
JAMI_WARN("Peer muted");
} else {
JAMI_WARN("Peer un-muted");
}
peerMuted_ = muted;
if (auto conf = Manager::instance().getConferenceFromID(getConfId())) {
conf->updateMuted();
}
}
void
SIPCall::resetMediaReady()
{
for (auto& m : mediaReady_)
m.second = false;
}
} // namespace jami