Ring Daemon
Loading...
Searching...
No Matches
sipcall.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#include "call_factory.h"
19#include "sip/sipcall.h"
20#include "sip/sipaccount.h"
21#include "sip/sipaccountbase.h"
22#include "sip/sipvoiplink.h"
23#include "jamidht/jamiaccount.h"
24#include "logger.h"
25#include "sdp.h"
26#include "manager.h"
27#include "string_utils.h"
28
32#include "jami/account_const.h"
33#include "jami/call_const.h"
34#include "jami/media_const.h"
35#include "client/jami_signal.h"
36#include "pjsip-ua/sip_inv.h"
37#include "video/video_mixer.h"
38
39#ifdef ENABLE_PLUGIN
41#endif
42
43#ifdef ENABLE_VIDEO
44#include "client/videomanager.h"
46#include <chrono>
47#include <libavutil/display.h>
48#include <video/sinkclient.h>
49#endif
51
52#include <dhtnet/upnp/upnp_control.h>
53#include <dhtnet/ice_transport_factory.h>
54
55#include <opendht/crypto.h>
56#include <opendht/thread_pool.h>
57#include <fmt/ranges.h>
58
59#include "tracepoint.h"
60
61namespace jami {
62
64using namespace libjami::Call;
65
66#ifdef ENABLE_VIDEO
67static DeviceParams
69{
71 return videomon->getDeviceParams(videomon->getDefaultDevice());
72 return DeviceParams {};
73}
74#endif
75
76static constexpr std::chrono::seconds DEFAULT_ICE_INIT_TIMEOUT {35}; // seconds
77static constexpr std::chrono::milliseconds EXPECTED_ICE_INIT_MAX_TIME {5000};
78static constexpr std::chrono::milliseconds MS_BETWEEN_2_KEYFRAME_REQUEST {1000};
79static constexpr int ICE_COMP_ID_RTP {1};
80static constexpr int ICE_COMP_COUNT_PER_STREAM {2};
81static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR = "10.0.2"sv;
82static const std::vector<unsigned> MULTISTREAM_REQUIRED_VERSION
84static constexpr auto MULTIICE_REQUIRED_VERSION_STR = "13.3.0"sv;
86 '.');
87static constexpr auto NEW_CONFPROTOCOL_VERSION_STR = "13.1.0"sv;
89 '.');
90static constexpr auto REUSE_ICE_IN_REINVITE_REQUIRED_VERSION_STR = "11.0.2"sv;
91static const std::vector<unsigned> REUSE_ICE_IN_REINVITE_REQUIRED_VERSION
93static constexpr auto MULTIAUDIO_REQUIRED_VERSION_STR = "13.11.0"sv;
94static const std::vector<unsigned> MULTIAUDIO_REQUIRED_VERSION
96
97SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
98 const std::string& callId,
99 Call::CallType type,
100 const std::vector<libjami::MediaMap>& mediaList)
101 : Call(account, callId, type)
102 , sdp_(new Sdp(callId))
103 , enableIce_(account->isIceForMediaEnabled())
104 , srtpEnabled_(account->isSrtpEnabled())
105{
106 jami_tracepoint(call_start, callId.c_str());
107
108 if (account->getUPnPActive())
109 upnp_ = std::make_shared<dhtnet::upnp::Controller>(Manager::instance().upnpContext());
110
111 setCallMediaLocal();
112
113 // Set the media caps.
114 sdp_->setLocalMediaCapabilities(MediaType::MEDIA_AUDIO, account->getActiveAccountCodecInfoList(MEDIA_AUDIO));
115#ifdef ENABLE_VIDEO
116 sdp_->setLocalMediaCapabilities(MediaType::MEDIA_VIDEO, account->getActiveAccountCodecInfoList(MEDIA_VIDEO));
117#endif
118
120
121 if (mediaAttrList.size() == 0) {
123 // Handle incoming call without media offer.
124 JAMI_WARNING("[call:{}] No media offered in the incoming invite. An offer will be provided in "
125 "the answer",
126 getCallId());
127 mediaAttrList = getSIPAccount()->createDefaultMediaList(false, getState() == CallState::HOLD);
128 } else {
129 JAMI_WARNING("[call:{}] Creating an outgoing call with empty offer", getCallId());
130 }
131 }
132
133 JAMI_DEBUG("[call:{:s}] Create a new [{:s}] SIP call with {:d} media",
134 getCallId(),
135 type == Call::CallType::INCOMING ? "INCOMING"
136 : (type == Call::CallType::OUTGOING ? "OUTGOING" : "MISSED"),
137 mediaList.size());
138
139 initMediaStreams(mediaAttrList);
140}
141
143{
144 std::lock_guard lk {callMutex_};
145
146 setSipTransport({});
147 setInviteSession(); // prevents callback usage
148
149#ifdef ENABLE_VIDEO
150 closeMediaPlayer(mediaPlayerId_);
151#endif
152}
153
154int
155SIPCall::findRtpStreamIndex(const std::string& label) const
156{
157 const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), [&label](const RtpStream& rtp) {
158 return label == rtp.mediaAttribute_->label_;
159 });
160
161 // Return the index if there is a match.
162 if (iter != rtpStreams_.end())
163 return static_cast<int>(std::distance(rtpStreams_.begin(), iter));
164
165 // No match found.
166 return -1;
167}
168
169void
170SIPCall::createRtpSession(RtpStream& stream)
171{
172 if (not stream.mediaAttribute_)
173 throw std::runtime_error("Missing media attribute");
174
175 // To get audio_0 ; video_0
176 auto streamId = sip_utils::streamId(id_, stream.mediaAttribute_->label_);
177 if (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO) {
178 stream.rtpSession_ = std::make_shared<AudioRtpSession>(id_, streamId, recorder_);
179 }
180#ifdef ENABLE_VIDEO
181 else if (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO) {
182 stream.rtpSession_ = std::make_shared<video::VideoRtpSession>(id_, streamId, getVideoSettings(), recorder_);
183 std::static_pointer_cast<video::VideoRtpSession>(stream.rtpSession_)->setRotation(rotation_);
184 }
185#endif
186 else {
187 throw std::runtime_error("Unsupported media type");
188 }
189
190 // Must be valid at this point.
191 if (not stream.rtpSession_)
192 throw std::runtime_error("Failed to create RTP session");
193 ;
194}
195
196void
197SIPCall::configureRtpSession(const std::shared_ptr<RtpSession>& rtpSession,
198 const std::shared_ptr<MediaAttribute>& mediaAttr,
199 const MediaDescription& localMedia,
200 const MediaDescription& remoteMedia)
201{
202 JAMI_DEBUG("[call:{}] Configuring [{}] RTP session",
203 getCallId(),
205
206 if (not rtpSession)
207 throw std::runtime_error("Must have a valid RTP session");
208
209 // Configure the media stream
210 auto new_mtu = sipTransport_->getTlsMtu();
211 rtpSession->setMtu(new_mtu);
212 rtpSession->updateMedia(remoteMedia, localMedia);
213
214 // Mute/un-mute media
215 rtpSession->setMuted(mediaAttr->muted_);
216
217 rtpSession->setMediaSource(mediaAttr->sourceUri_);
218
219 rtpSession->setSuccessfulSetupCb([w = weak()](MediaType, bool) {
220 // This sends SIP messages on socket, so move to io
221 dht::ThreadPool::io().run([w = std::move(w)] {
222 if (auto thisPtr = w.lock())
223 thisPtr->rtpSetupSuccess();
224 });
225 });
226
228 setupVoiceCallback(rtpSession);
229 }
230
231#ifdef ENABLE_VIDEO
233 auto videoRtp = std::dynamic_pointer_cast<video::VideoRtpSession>(rtpSession);
235 auto streamIdx = findRtpStreamIndex(mediaAttr->label_);
236 videoRtp->setRequestKeyFrameCallback([w = weak(), streamIdx] {
237 // This sends SIP messages on socket, so move to I/O
238 dht::ThreadPool::io().run([w = std::move(w), streamIdx] {
239 if (auto thisPtr = w.lock())
240 thisPtr->requestKeyframe(streamIdx);
241 });
242 });
243 videoRtp->setChangeOrientationCallback([w = weak(), streamIdx](int angle) {
244 // This sends SIP messages on socket, so move to I/O
245 dht::ThreadPool::io().run([w, angle, streamIdx] {
246 if (auto thisPtr = w.lock())
247 thisPtr->setVideoOrientation(streamIdx, angle);
248 });
249 });
250 }
251#endif
252}
253
254void
255SIPCall::setupVoiceCallback(const std::shared_ptr<RtpSession>& rtpSession)
256{
257 // need to downcast to access setVoiceCallback
258 auto audioRtp = std::dynamic_pointer_cast<AudioRtpSession>(rtpSession);
259
260 audioRtp->setVoiceCallback([w = weak()](bool voice) {
261 // this is called whenever voice is detected on the local audio
262
264 if (auto thisPtr = w.lock()) {
265 // TODO: once we support multiple streams, change this to the right one
266 std::string streamId = "";
267
268#ifdef ENABLE_VIDEO
269 if (auto* videoManager = Manager::instance().getVideoManager()) {
270 if (not videoManager->videoDeviceMonitor.getDeviceList().empty()) {
271 // if we have a video device
272 streamId = sip_utils::streamId("", sip_utils::DEFAULT_VIDEO_STREAMID);
273 }
274 }
275#endif
276
277 // send our local voice activity
278 if (auto conference = thisPtr->conf_.lock()) {
279 // we are in a conference
280
281 // updates conference info and sends it to others via ConfInfo
282 // (only if there was a change)
283 // also emits signal with updated conference info
284 conference->setVoiceActivity(streamId, voice);
285 } else {
286 // we are in a one-to-one call
287 // send voice activity over SIP
288 // TODO: change the streamID once multiple streams are supported
289 thisPtr->sendVoiceActivity("-1", voice);
290
291 // TODO: maybe emit signal here for local voice activity
292 }
293 } else {
294 JAMI_ERROR("Voice activity callback unable to lock weak ptr to SIPCall");
295 }
296 });
297 });
298}
299
300std::shared_ptr<SIPAccountBase>
301SIPCall::getSIPAccount() const
302{
303 return std::static_pointer_cast<SIPAccountBase>(getAccount().lock());
304}
305
306#ifdef ENABLE_PLUGIN
307void
308SIPCall::createCallAVStreams()
309{
310#ifdef ENABLE_VIDEO
311 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO)) {
312 if (std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->hasConference()) {
313 clearCallAVStreams();
314 return;
315 }
316 }
317#endif
318
319 auto baseId = getCallId();
320 auto mediaMap = [](const std::shared_ptr<jami::MediaFrame>& m) -> AVFrame* {
321 return m->pointer();
322 };
323
324 std::lock_guard lk(avStreamsMtx_);
325 for (const auto& rtpSession : getRtpSessionList()) {
326 auto isVideo = rtpSession->getMediaType() == MediaType::MEDIA_VIDEO;
327 auto streamType = isVideo ? StreamType::video : StreamType::audio;
328 StreamData previewStreamData {baseId, false, streamType, getPeerNumber(), getAccountId()};
329 StreamData receiveStreamData {baseId, true, streamType, getPeerNumber(), getAccountId()};
330#ifdef ENABLE_VIDEO
331 if (isVideo) {
332 // Preview
333 auto videoRtp = std::static_pointer_cast<video::VideoRtpSession>(rtpSession);
334 if (auto& videoPreview = videoRtp->getVideoLocal())
335 createCallAVStream(previewStreamData, *videoPreview, std::make_shared<MediaStreamSubject>(mediaMap));
336 // Receive
337 if (auto& videoReceive = videoRtp->getVideoReceive())
338 createCallAVStream(receiveStreamData, *videoReceive, std::make_shared<MediaStreamSubject>(mediaMap));
339 } else {
340#endif
341 auto audioRtp = std::static_pointer_cast<AudioRtpSession>(rtpSession);
342 // Preview
343 if (auto& localAudio = audioRtp->getAudioLocal())
344 createCallAVStream(previewStreamData, *localAudio, std::make_shared<MediaStreamSubject>(mediaMap));
345 // Receive
346 if (auto& audioReceive = audioRtp->getAudioReceive())
347 createCallAVStream(receiveStreamData,
348 (AVMediaStream&) *audioReceive,
349 std::make_shared<MediaStreamSubject>(mediaMap));
350#ifdef ENABLE_VIDEO
351 }
352#endif
353 }
354}
355
356void
357SIPCall::createCallAVStream(const StreamData& streamData,
358 AVMediaStream& streamSource,
359 const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject)
360{
361 const std::string AVStreamId = streamData.id + std::to_string(static_cast<int>(streamData.type))
362 + std::to_string(streamData.direction);
363 auto it = callAVStreams.find(AVStreamId);
364 if (it != callAVStreams.end())
365 return;
366 it = callAVStreams.insert(it, {AVStreamId, mediaStreamSubject});
367 streamSource.attachPriorityObserver(it->second);
368 jami::Manager::instance().getJamiPluginManager().getCallServicesManager().createAVSubject(streamData, it->second);
369}
370
371void
372SIPCall::clearCallAVStreams()
373{
374 std::lock_guard lk(avStreamsMtx_);
375 callAVStreams.clear();
376}
377#endif // ENABLE_PLUGIN
378
379void
380SIPCall::setCallMediaLocal()
381{
382 if (localAudioPort_ == 0
383#ifdef ENABLE_VIDEO
384 || localVideoPort_ == 0
385#endif
386 )
387 generateMediaPorts();
388}
389
390void
391SIPCall::generateMediaPorts()
392{
393 auto account = getSIPAccount();
394 if (!account) {
395 JAMI_ERROR("[call:{}] No account detected", getCallId());
396 return;
397 }
398
399 // TODO. Setting specfic range for RTP ports is obsolete, in
400 // particular in the context of ICE.
401
402 // Reference: http://www.cs.columbia.edu/~hgs/rtp/faq.html#ports
403 // We only want to set ports to new values if they haven't been set
404 const unsigned callLocalAudioPort = account->generateAudioPort();
405 if (localAudioPort_ != 0)
406 account->releasePort(localAudioPort_);
407 localAudioPort_ = callLocalAudioPort;
408 sdp_->setLocalPublishedAudioPorts(callLocalAudioPort, rtcpMuxEnabled_ ? 0 : callLocalAudioPort + 1);
409
410#ifdef ENABLE_VIDEO
411 // https://projects.savoirfairelinux.com/issues/17498
412 const unsigned int callLocalVideoPort = account->generateVideoPort();
413 if (localVideoPort_ != 0)
414 account->releasePort(localVideoPort_);
415 // this should already be guaranteed by SIPAccount
416 assert(localAudioPort_ != callLocalVideoPort);
417 localVideoPort_ = callLocalVideoPort;
418 sdp_->setLocalPublishedVideoPorts(callLocalVideoPort, rtcpMuxEnabled_ ? 0 : callLocalVideoPort + 1);
419#endif
420}
421
422const std::string&
423SIPCall::getContactHeader() const
424{
425 return contactHeader_;
426}
427
428void
429SIPCall::setSipTransport(const std::shared_ptr<SipTransport>& transport, const std::string& contactHdr)
430{
431 if (transport != sipTransport_) {
432 JAMI_DEBUG("[call:{}] Setting transport to [{}]", getCallId(), fmt::ptr(transport.get()));
433 }
434
435 sipTransport_ = transport;
436 contactHeader_ = contactHdr;
437
438 if (not transport) {
439 // Done.
440 return;
441 }
442
443 if (contactHeader_.empty()) {
444 JAMI_WARNING("[call:{}] Contact header is empty", getCallId());
445 }
446
447 if (isSrtpEnabled() and not sipTransport_->isSecure()) {
448 JAMI_WARNING("[call:{}] Crypto (SRTP) is negotiated over an unencrypted signaling channel", getCallId());
449 }
450
451 if (not isSrtpEnabled() and sipTransport_->isSecure()) {
452 JAMI_WARNING("[call:{}] The signaling channel is encrypted but the media is unencrypted", getCallId());
453 }
454
455 const auto list_id = reinterpret_cast<uintptr_t>(this);
456 sipTransport_->removeStateListener(list_id);
457
458 // Listen for transport destruction
459 sipTransport_
460 ->addStateListener(list_id, [wthis_ = weak()](pjsip_transport_state state, const pjsip_transport_state_info*) {
461 if (auto this_ = wthis_.lock()) {
462 JAMI_DEBUG("[call:{}] SIP transport state [{}] - connection state [{}]",
463 this_->getCallId(),
464 static_cast<int>(state),
465 static_cast<unsigned>(this_->getConnectionState()));
466
467 // End the call if the SIP transport was shut down
468 auto isAlive = SipTransport::isAlive(state);
469 if (not isAlive and this_->getConnectionState() != ConnectionState::DISCONNECTED) {
470 JAMI_WARNING("[call:{}] Ending call because underlying SIP transport was closed",
471 this_->getCallId());
472 this_->stopAllMedia();
473 this_->detachAudioFromConference();
474 this_->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
475 }
476 }
477 });
478}
479
480void
481SIPCall::requestReinvite(const std::vector<MediaAttribute>& mediaAttrList, bool needNewIce)
482{
483 JAMI_DEBUG("[call:{}] Sending a SIP re-invite to request media change", getCallId());
484
485 if (isWaitingForIceAndMedia_) {
486 remainingRequest_ = Request::SwitchInput;
487 } else {
488 if (SIPSessionReinvite(mediaAttrList, needNewIce) == PJ_SUCCESS and reinvIceMedia_) {
489 isWaitingForIceAndMedia_ = true;
490 }
491 }
492}
493
498int
499SIPCall::SIPSessionReinvite(const std::vector<MediaAttribute>& mediaAttrList, bool needNewIce)
500{
501 assert(not mediaAttrList.empty());
502
503 std::lock_guard lk {callMutex_};
504
505 // Do nothing if no invitation processed yet
506 if (not inviteSession_ or inviteSession_->invite_tsx)
507 return PJ_SUCCESS;
508
509 JAMI_DEBUG("[call:{}] Preparing and sending a re-invite (state={})",
510 getCallId(),
511 pjsip_inv_state_name(inviteSession_->state));
512 JAMI_DEBUG("[call:{}] New ICE required for this re-invite: [{}]", getCallId(), needNewIce ? "Yes" : "No");
513
514 // Generate new ports to receive the new media stream
515 // LibAV doesn't discriminate SSRCs and will be confused about Seq changes on a given port
516 generateMediaPorts();
517
518 sdp_->clearIce();
519 sdp_->setActiveRemoteSdpSession(nullptr);
520 sdp_->setActiveLocalSdpSession(nullptr);
521
522 auto acc = getSIPAccount();
523 if (not acc) {
524 JAMI_ERROR("[call:{}] No account detected", getCallId());
525 return !PJ_SUCCESS;
526 }
527
528 if (not sdp_->createOffer(mediaAttrList))
529 return !PJ_SUCCESS;
530
531 if (isIceEnabled() and needNewIce) {
532 if (not createIceMediaTransport(true) or not initIceMediaTransport(true)) {
533 return !PJ_SUCCESS;
534 }
535 addLocalIceAttributes();
536 // Media transport changed, must restart the media.
537 mediaRestartRequired_ = true;
538 }
539
540 pjsip_tx_data* tdata;
541 auto* local_sdp = sdp_->getLocalSdpSession();
542 auto result = pjsip_inv_reinvite(inviteSession_.get(), nullptr, local_sdp, &tdata);
543 if (result == PJ_SUCCESS) {
544 if (!tdata)
545 return PJ_SUCCESS;
546
547 // Add user-agent header
548 sip_utils::addUserAgentHeader(acc->getUserAgentName(), tdata);
549
550 result = pjsip_inv_send_msg(inviteSession_.get(), tdata);
551 if (result == PJ_SUCCESS)
552 return PJ_SUCCESS;
553 JAMI_ERROR("[call:{}] Failed to send REINVITE msg (pjsip: {})", getCallId(), sip_utils::sip_strerror(result));
554 // Canceling internals without sending (anyways the send has just failed!)
555 pjsip_inv_cancel_reinvite(inviteSession_.get(), &tdata);
556 } else
557 JAMI_ERROR("[call:{}] Failed to create REINVITE msg (pjsip: {})", getCallId(), sip_utils::sip_strerror(result));
558
559 return !PJ_SUCCESS;
560}
561
562int
563SIPCall::SIPSessionReinvite()
564{
565 auto mediaList = getMediaAttributeList();
566 return SIPSessionReinvite(mediaList, isNewIceMediaRequired(mediaList));
567}
568
569void
570SIPCall::sendSIPInfo(std::string_view body, std::string_view subtype)
571{
572 std::lock_guard lk {callMutex_};
573 if (not inviteSession_ or not inviteSession_->dlg)
574 throw VoipLinkException("Unable to get invite dialog");
575
576 constexpr pj_str_t methodName = CONST_PJ_STR("INFO");
577 constexpr pj_str_t type = CONST_PJ_STR("application");
578
579 pjsip_method method;
580 pjsip_method_init_np(&method, (pj_str_t*) &methodName);
581
582 /* Create request message. */
583 pjsip_tx_data* tdata;
584 if (pjsip_dlg_create_request(inviteSession_->dlg, &method, -1, &tdata) != PJ_SUCCESS) {
585 JAMI_ERROR("[call:{}] Unable to create dialog", getCallId());
586 return;
587 }
588
589 /* Create "application/<subtype>" message body. */
590 pj_str_t content = CONST_PJ_STR(body);
591 pj_str_t pj_subtype = CONST_PJ_STR(subtype);
592 tdata->msg->body = pjsip_msg_body_create(tdata->pool, &type, &pj_subtype, &content);
593 if (tdata->msg->body == NULL)
594 pjsip_tx_data_dec_ref(tdata);
595 else
596 pjsip_dlg_send_request(inviteSession_->dlg, tdata, Manager::instance().sipVoIPLink().getModId(), NULL);
597}
598
599void
600SIPCall::updateRecState(bool state)
601{
602 std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
603 "<media_control><vc_primitive><to_encoder>"
604 "<recording_state="
605 + std::to_string(state)
606 + "/>"
607 "</to_encoder></vc_primitive></media_control>";
608 // see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
609
610 JAMI_DEBUG("[call:{}] Sending recording state via SIP INFO", getCallId());
611
612 try {
613 sendSIPInfo(BODY, "media_control+xml");
614 } catch (const std::exception& e) {
615 JAMI_ERROR("[call:{}] Error sending recording state: {}", getCallId(), e.what());
616 }
617}
618
619void
620SIPCall::requestKeyframe(int streamIdx)
621{
622 auto now = clock::now();
623 if ((now - lastKeyFrameReq_) < MS_BETWEEN_2_KEYFRAME_REQUEST and lastKeyFrameReq_ != time_point::min())
624 return;
625
626 std::string streamIdPart;
627 if (streamIdx != -1)
628 streamIdPart = fmt::format("<stream_id>{}</stream_id>", streamIdx);
629 std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
630 "<media_control><vc_primitive> "
631 + streamIdPart + "<to_encoder>"
632 + "<picture_fast_update/>"
633 "</to_encoder></vc_primitive></media_control>";
634 JAMI_DEBUG("[call:{}] Sending video keyframe request via SIP INFO", getCallId());
635 try {
636 sendSIPInfo(BODY, "media_control+xml");
637 } catch (const std::exception& e) {
638 JAMI_ERROR("[call:{}] Error sending video keyframe request: {}", getCallId(), e.what());
639 }
640 lastKeyFrameReq_ = now;
641}
642
643void
644SIPCall::sendMuteState(bool state)
645{
646 std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
647 "<media_control><vc_primitive><to_encoder>"
648 "<mute_state="
649 + std::to_string(state)
650 + "/>"
651 "</to_encoder></vc_primitive></media_control>";
652 // see https://tools.ietf.org/html/rfc5168 for XML Schema for Media Control details
653
654 JAMI_DEBUG("[call:{}] Sending mute state via SIP INFO", getCallId());
655
656 try {
657 sendSIPInfo(BODY, "media_control+xml");
658 } catch (const std::exception& e) {
659 JAMI_ERROR("[call:{}] Error sending mute state: {}", getCallId(), e.what());
660 }
661}
662
663void
664SIPCall::sendVoiceActivity(std::string_view streamId, bool state)
665{
666 // dont send streamId if it's -1
667 std::string streamIdPart = "";
668 if (streamId != "-1" && !streamId.empty()) {
669 streamIdPart = fmt::format("<stream_id>{}</stream_id>", streamId);
670 }
671
672 std::string BODY = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
673 "<media_control><vc_primitive>"
674 + streamIdPart
675 + "<to_encoder>"
676 "<voice_activity="
677 + std::to_string(state)
678 + "/>"
679 "</to_encoder></vc_primitive></media_control>";
680
681 try {
682 sendSIPInfo(BODY, "media_control+xml");
683 } catch (const std::exception& e) {
684 JAMI_ERROR("[call:{}] Error sending voice activity state: {}", getCallId(), e.what());
685 }
686}
687
688void
689SIPCall::setInviteSession(pjsip_inv_session* inviteSession)
690{
691 std::lock_guard lk {callMutex_};
692
693 if (inviteSession == nullptr and inviteSession_) {
694 JAMI_DEBUG("[call:{}] Delete current invite session", getCallId());
695 } else if (inviteSession != nullptr) {
696 // NOTE: The first reference of the invite session is owned by pjsip. If
697 // that counter goes down to zero the invite will be destroyed, and the
698 // unique_ptr will point freed datas. To avoid this, we increment the
699 // ref counter and let our unique_ptr share the ownership of the session
700 // with pjsip.
701 if (PJ_SUCCESS != pjsip_inv_add_ref(inviteSession)) {
702 JAMI_WARNING("[call:{}] Attempting to set invalid invite session [{}]",
703 getCallId(),
704 fmt::ptr(inviteSession));
705 inviteSession_.reset(nullptr);
706 return;
707 }
708 JAMI_DEBUG("[call:{}] Set new invite session [{}]", getCallId(), fmt::ptr(inviteSession));
709 } else {
710 // Nothing to do.
711 return;
712 }
713
714 inviteSession_.reset(inviteSession);
715}
716
717void
718SIPCall::terminateSipSession(int status)
719{
720 JAMI_DEBUG("[call:{}] Terminate SIP session", getCallId());
721 std::lock_guard lk {callMutex_};
722 if (inviteSession_ and inviteSession_->state != PJSIP_INV_STATE_DISCONNECTED) {
723 pjsip_tx_data* tdata = nullptr;
724 auto ret = pjsip_inv_end_session(inviteSession_.get(), status, nullptr, &tdata);
725 if (ret == PJ_SUCCESS) {
726 if (tdata) {
727 auto account = getSIPAccount();
728 if (account) {
729 sip_utils::addContactHeader(contactHeader_, tdata);
730 // Add user-agent header
731 sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
732 } else {
733 JAMI_ERROR("[call:{}] No account detected", getCallId());
734 throw std::runtime_error(
735 fmt::format("[call:{}] The account owning this call is invalid", getCallId()));
736 }
737
738 ret = pjsip_inv_send_msg(inviteSession_.get(), tdata);
739 if (ret != PJ_SUCCESS)
740 JAMI_ERROR("[call:{}] Failed to send terminate msg, SIP error ({})",
741 getCallId(),
742 sip_utils::sip_strerror(ret));
743 }
744 } else
745 JAMI_ERROR("[call:{}] Failed to terminate INVITE@{}, SIP error ({})",
746 getCallId(),
747 fmt::ptr(inviteSession_.get()),
748 sip_utils::sip_strerror(ret));
749 }
750 setInviteSession();
751}
752
753void
754SIPCall::answer(const std::vector<libjami::MediaMap>& mediaList)
755{
756 std::lock_guard lk {callMutex_};
757 auto account = getSIPAccount();
758 if (not account) {
759 JAMI_ERROR("[call:{}] No account detected", getCallId());
760 return;
761 }
762
763 if (not inviteSession_) {
764 JAMI_ERROR("[call:{}] No invite session for this call", getCallId());
765 return;
766 }
767
768 if (not sdp_) {
769 JAMI_ERROR("[call:{}] No SDP session for this call", getCallId());
770 return;
771 }
772
773 auto newMediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
774
775 if (newMediaAttrList.empty() and rtpStreams_.empty()) {
776 JAMI_ERROR("[call:{}] Media list must not be empty!", getCallId());
777 return;
778 }
779
780 // If the media list is empty, use the current media (this could happen
781 // with auto-answer for instance), otherwise update the current media.
782 if (newMediaAttrList.empty()) {
783 JAMI_DEBUG("[call:{}] Media list is empty, using current media", getCallId());
784 } else if (newMediaAttrList.size() != rtpStreams_.size()) {
785 // This should never happen, as we make sure that the sizes match earlier
786 // in handleIncomingConversationCall.
787 JAMI_ERROR("[call:{:s}] Media list size {:d} in answer does not match. Expected {:d}",
788 getCallId(),
789 newMediaAttrList.size(),
790 rtpStreams_.size());
791 return;
792 }
793
794 auto const& mediaAttrList = newMediaAttrList.empty() ? getMediaAttributeList() : newMediaAttrList;
795
796 JAMI_DEBUG("[call:{}] Answering incoming call with following media:", getCallId());
797 for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
798 auto const& mediaAttr = mediaAttrList.at(idx);
799 JAMI_DEBUG("[call:{:s}] Media @{:d} - {:s}", getCallId(), idx, mediaAttr.toString(true));
800 }
801
802 // Apply the media attributes.
803 for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
804 updateMediaStream(mediaAttrList[idx], idx);
805 }
806
807 // Create the SDP answer
808 sdp_->processIncomingOffer(mediaAttrList);
809
810 if (isIceEnabled() and remoteHasValidIceAttributes()) {
811 setupIceResponse();
812 }
813
814 if (not inviteSession_->neg) {
815 // We are answering to an INVITE that did not include a media offer (SDP).
816 // The SIP specification (RFCs 3261/6337) requires that if a UA wants to
817 // proceed with the call, it must provide a media offer (SDP) if the initial
818 // INVITE did not offer one. In this case, the SDP offer will be included in
819 // the SIP OK (200) answer. The peer UA will then include its SDP answer in
820 // the SIP ACK message.
821
822 // TODO. This code should be unified with the code used by accounts to create
823 // SDP offers.
824
825 JAMI_WARNING("[call:{}] No negotiator session, peer sent an empty INVITE (without SDP)", getCallId());
826
827 Manager::instance().sipVoIPLink().createSDPOffer(inviteSession_.get());
828
829 generateMediaPorts();
830
831 // Setup and create ICE offer
832 if (isIceEnabled()) {
833 sdp_->clearIce();
834 sdp_->setActiveRemoteSdpSession(nullptr);
835 sdp_->setActiveLocalSdpSession(nullptr);
836
837 auto opts = account->getIceOptions();
838
839 auto publicAddr = account->getPublishedIpAddress();
840
841 if (publicAddr) {
842 opts.accountPublicAddr = publicAddr;
843 if (auto interfaceAddr = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(),
844 publicAddr.getFamily())) {
845 opts.accountLocalAddr = interfaceAddr;
846 if (createIceMediaTransport(false) and initIceMediaTransport(true, std::move(opts))) {
847 addLocalIceAttributes();
848 }
849 } else {
850 JAMI_WARNING("[call:{}] Unable to init ICE transport, missing local address", getCallId());
851 }
852 } else {
853 JAMI_WARNING("[call:{}] Unable to init ICE transport, missing public address", getCallId());
854 }
855 }
856 }
857
858 if (!inviteSession_->last_answer)
859 throw std::runtime_error("Should only be called for initial answer");
860
861 // Set the SIP final answer (200 OK).
862 pjsip_tx_data* tdata;
863 if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, sdp_->getLocalSdpSession(), &tdata) != PJ_SUCCESS)
864 throw std::runtime_error("Unable to init invite request answer (200 OK)");
865
866 if (contactHeader_.empty()) {
867 throw std::runtime_error("Unable to answer with an invalid contact header");
868 }
869
870 JAMI_DEBUG("[call:{}] Answering with contact header: {}", getCallId(), contactHeader_);
871
872 sip_utils::addContactHeader(contactHeader_, tdata);
873
874 // Add user-agent header
875 sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
876
877 if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
878 setInviteSession();
879 throw std::runtime_error("Unable to send invite request answer (200 OK)");
880 }
881
882 setState(CallState::ACTIVE, ConnectionState::CONNECTED);
883}
884
885void
886SIPCall::answerMediaChangeRequest(const std::vector<libjami::MediaMap>& mediaList, bool isRemote)
887{
888 std::lock_guard lk {callMutex_};
889
890 auto account = getSIPAccount();
891 if (not account) {
892 JAMI_ERROR("[call:{}] No account detected", getCallId());
893 return;
894 }
895
896 auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
897
898 // TODO. is the right place?
899 // Disable video if disabled in the account.
900 if (not account->isVideoEnabled()) {
901 for (auto& mediaAttr : mediaAttrList) {
902 if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
903 mediaAttr.enabled_ = false;
904 }
905 }
906 }
907
908 if (mediaAttrList.empty()) {
909 JAMI_WARNING("[call:{}] Media list is empty. Ignoring the media change request", getCallId());
910 return;
911 }
912
913 if (not sdp_) {
914 JAMI_ERROR("[call:{}] No valid SDP session", getCallId());
915 return;
916 }
917
918 JAMI_DEBUG("[call:{}] Current media", getCallId());
919 unsigned idx = 0;
920 for (auto const& rtp : rtpStreams_) {
921 JAMI_DEBUG("[call:{}] Media @{}: {}", getCallId(), idx++, rtp.mediaAttribute_->toString(true));
922 }
923
924 JAMI_DEBUG("[call:{}] Answering to media change request with new media", getCallId());
925 idx = 0;
926 for (auto const& newMediaAttr : mediaAttrList) {
927 JAMI_DEBUG("[call:{}] Media @{}: {}", getCallId(), idx++, newMediaAttr.toString(true));
928 }
929
930 if (!updateAllMediaStreams(mediaAttrList, isRemote))
931 return;
932
933 if (not sdp_->processIncomingOffer(mediaAttrList)) {
934 JAMI_WARNING("[call:{}] Unable to process the new offer, ignoring", getCallId());
935 return;
936 }
937
938 if (not sdp_->getRemoteSdpSession()) {
939 JAMI_ERROR("[call:{}] No valid remote SDP session", getCallId());
940 return;
941 }
942
943 if (isIceEnabled() and remoteHasValidIceAttributes()) {
944 JAMI_WARNING("[call:{}] Requesting a new ICE media", getCallId());
945 setupIceResponse(true);
946 }
947
948 if (not sdp_->startNegotiation()) {
949 JAMI_ERROR("[call:{}] Unable to start media negotiation for a re-invite request", getCallId());
950 return;
951 }
952
953 if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
954 JAMI_ERROR("[call:{}] Unable to start media negotiation for a re-invite request", getCallId());
955 return;
956 }
957
958 pjsip_tx_data* tdata;
959 if (pjsip_inv_answer(inviteSession_.get(), PJSIP_SC_OK, NULL, NULL, &tdata) != PJ_SUCCESS) {
960 JAMI_ERROR("[call:{}] Unable to init answer to a re-invite request", getCallId());
961 return;
962 }
963
964 if (not contactHeader_.empty()) {
965 sip_utils::addContactHeader(contactHeader_, tdata);
966 }
967
968 // Add user-agent header
969 sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);
970
971 if (pjsip_inv_send_msg(inviteSession_.get(), tdata) != PJ_SUCCESS) {
972 JAMI_ERROR("[call:{}] Unable to send answer to a re-invite request", getCallId());
973 setInviteSession();
974 return;
975 }
976
977 JAMI_DEBUG("[call:{}] Successfully answered the media change request", getCallId());
978}
979
980void
981SIPCall::hangup(int code)
982{
983 std::lock_guard lk {callMutex_};
984 pendingRecord_ = false;
985 if (inviteSession_ and inviteSession_->dlg) {
986 pjsip_route_hdr* route = inviteSession_->dlg->route_set.next;
987 while (route and route != &inviteSession_->dlg->route_set) {
988 char buf[1024];
989 int printed = pjsip_hdr_print_on(route, buf, sizeof(buf));
990 if (printed >= 0) {
991 buf[printed] = '\0';
992 JAMI_DEBUG("[call:{}] Route header {}", getCallId(), buf);
993 }
994 route = route->next;
995 }
996
997 int status = PJSIP_SC_OK;
998 if (code)
999 status = code;
1000 else if (inviteSession_->state <= PJSIP_INV_STATE_EARLY and inviteSession_->role != PJSIP_ROLE_UAC)
1001 status = PJSIP_SC_CALL_TSX_DOES_NOT_EXIST;
1002 else if (inviteSession_->state >= PJSIP_INV_STATE_DISCONNECTED)
1003 status = PJSIP_SC_DECLINE;
1004
1005 // Notify the peer
1006 terminateSipSession(status);
1007 }
1008
1009 // Stop all RTP streams
1010 stopAllMedia();
1011 detachAudioFromConference();
1012 setState(Call::ConnectionState::DISCONNECTED, code);
1013 dht::ThreadPool::io().run([w = weak(), code] {
1014 if (auto shared = w.lock())
1015 shared->removeCall(code);
1016 });
1017}
1018
1019void
1020SIPCall::detachAudioFromConference()
1021{
1022#ifdef ENABLE_VIDEO
1023 if (auto conf = getConference()) {
1024 if (auto mixer = conf->getVideoMixer()) {
1025 for (auto& stream : getRtpSessionList(MediaType::MEDIA_AUDIO)) {
1026 mixer->removeAudioOnlySource(getCallId(), stream->streamId());
1027 }
1028 }
1029 }
1030#endif
1031}
1032
1033void
1034SIPCall::refuse()
1035{
1036 if (!isIncoming() or getConnectionState() == ConnectionState::CONNECTED or !inviteSession_)
1037 return;
1038
1039 stopAllMedia();
1040
1041 // Notify the peer
1042 terminateSipSession(PJSIP_SC_DECLINE);
1043
1044 setState(Call::ConnectionState::DISCONNECTED, PJSIP_SC_DECLINE);
1045 removeCall();
1046}
1047
1048static void
1049transfer_client_cb(pjsip_evsub* sub, pjsip_event* event)
1050{
1051 auto mod_ua_id = Manager::instance().sipVoIPLink().getModId();
1052
1053 switch (pjsip_evsub_get_state(sub)) {
1054 case PJSIP_EVSUB_STATE_ACCEPTED:
1055 if (!event)
1056 return;
1057
1058 pj_assert(event->type == PJSIP_EVENT_TSX_STATE && event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
1059 break;
1060
1061 case PJSIP_EVSUB_STATE_TERMINATED:
1062 pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
1063 break;
1064
1065 case PJSIP_EVSUB_STATE_ACTIVE: {
1066 if (!event)
1067 return;
1068
1069 pjsip_rx_data* r_data = event->body.rx_msg.rdata;
1070
1071 if (!r_data)
1072 return;
1073
1074 std::string request(pjsip_rx_data_get_info(r_data));
1075
1076 pjsip_status_line status_line = {500, *pjsip_get_status_text(500)};
1077
1078 if (!r_data->msg_info.msg)
1079 return;
1080
1081 if (r_data->msg_info.msg->line.req.method.id == PJSIP_OTHER_METHOD
1082 and request.find("NOTIFY") != std::string::npos) {
1083 pjsip_msg_body* body = r_data->msg_info.msg->body;
1084
1085 if (!body)
1086 return;
1087
1088 if (pj_stricmp2(&body->content_type.type, "message") or pj_stricmp2(&body->content_type.subtype, "sipfrag"))
1089 return;
1090
1091 if (pjsip_parse_status_line((char*) body->data, body->len, &status_line) != PJ_SUCCESS)
1092 return;
1093 }
1094
1095 if (!r_data->msg_info.cid)
1096 return;
1097
1098 auto* call = static_cast<SIPCall*>(pjsip_evsub_get_mod_data(sub, mod_ua_id));
1099 if (!call)
1100 return;
1101
1102 if (status_line.code / 100 == 2) {
1103 if (call->inviteSession_)
1104 call->terminateSipSession(PJSIP_SC_GONE);
1105 Manager::instance().hangupCall(call->getAccountId(), call->getCallId());
1106 pjsip_evsub_set_mod_data(sub, mod_ua_id, NULL);
1107 }
1108
1109 break;
1110 }
1111
1112 case PJSIP_EVSUB_STATE_NULL:
1113 case PJSIP_EVSUB_STATE_SENT:
1114 case PJSIP_EVSUB_STATE_PENDING:
1115 case PJSIP_EVSUB_STATE_UNKNOWN:
1116 default:
1117 break;
1118 }
1119}
1120
1121bool
1122SIPCall::transferCommon(const pj_str_t* dst)
1123{
1124 if (not inviteSession_ or not inviteSession_->dlg)
1125 return false;
1126
1127 pjsip_evsub_user xfer_cb;
1128 pj_bzero(&xfer_cb, sizeof(xfer_cb));
1129 xfer_cb.on_evsub_state = &transfer_client_cb;
1130
1131 pjsip_evsub* sub;
1132
1133 if (pjsip_xfer_create_uac(inviteSession_->dlg, &xfer_cb, &sub) != PJ_SUCCESS)
1134 return false;
1135
1136 /* Associate this VoIPLink of call with the client subscription
1137 * We are unable to just associate call with the client subscription
1138 * because after this function, we are unable to find the corresponding
1139 * VoIPLink from the call any more. But the VoIPLink is useful!
1140 */
1141 pjsip_evsub_set_mod_data(sub, Manager::instance().sipVoIPLink().getModId(), this);
1142
1143 /*
1144 * Create REFER request.
1145 */
1146 pjsip_tx_data* tdata;
1147
1148 if (pjsip_xfer_initiate(sub, dst, &tdata) != PJ_SUCCESS)
1149 return false;
1150
1151 /* Send. */
1152 if (pjsip_xfer_send_request(sub, tdata) != PJ_SUCCESS)
1153 return false;
1154
1155 return true;
1156}
1157
1158void
1159SIPCall::transfer(const std::string& to)
1160{
1161 auto account = getSIPAccount();
1162 if (!account) {
1163 JAMI_ERROR("[call:{}] No account detected", getCallId());
1164 return;
1165 }
1166
1167 deinitRecorder();
1168 if (Call::isRecording())
1169 stopRecording();
1170
1171 std::string toUri = account->getToUri(to);
1172 const pj_str_t dst(CONST_PJ_STR(toUri));
1173
1174 JAMI_DEBUG("[call:{}] Transferring to {}", getCallId(), std::string_view(dst.ptr, dst.slen));
1175
1176 if (!transferCommon(&dst))
1177 throw VoipLinkException("Unable to transfer");
1178}
1179
1180bool
1181SIPCall::attendedTransfer(const std::string& to)
1182{
1183 auto toCall = Manager::instance().callFactory.getCall<SIPCall>(to);
1184 if (!toCall)
1185 return false;
1186
1187 if (not toCall->inviteSession_ or not toCall->inviteSession_->dlg)
1188 return false;
1189
1190 pjsip_dialog* target_dlg = toCall->inviteSession_->dlg;
1191 pjsip_uri* uri = (pjsip_uri*) pjsip_uri_get_uri(target_dlg->remote.info->uri);
1192
1193 char str_dest_buf[PJSIP_MAX_URL_SIZE * 2] = {'<'};
1194 pj_str_t dst = {str_dest_buf, 1};
1195
1196 dst.slen += pjsip_uri_print(PJSIP_URI_IN_REQ_URI, uri, str_dest_buf + 1, sizeof(str_dest_buf) - 1);
1197 dst.slen += pj_ansi_snprintf(str_dest_buf + dst.slen,
1198 sizeof(str_dest_buf) - dst.slen,
1199 "?"
1200 "Replaces=%.*s"
1201 "%%3Bto-tag%%3D%.*s"
1202 "%%3Bfrom-tag%%3D%.*s>",
1203 (int) target_dlg->call_id->id.slen,
1204 target_dlg->call_id->id.ptr,
1205 (int) target_dlg->remote.info->tag.slen,
1206 target_dlg->remote.info->tag.ptr,
1207 (int) target_dlg->local.info->tag.slen,
1208 target_dlg->local.info->tag.ptr);
1209
1210 return transferCommon(&dst);
1211}
1212
1213bool
1214SIPCall::hold(OnReadyCb&& cb)
1215{
1216 // If ICE is currently negotiating, we must wait before hold the call
1217 if (isWaitingForIceAndMedia_) {
1218 holdCb_ = std::move(cb);
1219 remainingRequest_ = Request::Hold;
1220 return false;
1221 }
1222
1223 auto result = hold();
1224
1225 if (cb)
1226 cb(result);
1227
1228 return result;
1229}
1230
1231bool
1232SIPCall::hold()
1233{
1234 if (getConnectionState() != ConnectionState::CONNECTED) {
1235 JAMI_WARNING("[call:{}] Not connected, ignoring hold request", getCallId());
1236 return false;
1237 }
1238
1239 if (not setState(CallState::HOLD)) {
1240 JAMI_WARNING("[call:{}] Failed to set state to HOLD", getCallId());
1241 return false;
1242 }
1243
1244 stopAllMedia();
1245
1246 for (auto& stream : rtpStreams_) {
1247 stream.mediaAttribute_->hold_ = true;
1248 }
1249
1250 if (SIPSessionReinvite() != PJ_SUCCESS) {
1251 JAMI_WARNING("[call:{}] Reinvite failed", getCallId());
1252 return false;
1253 }
1254
1255 // TODO. Do we need to check for reinvIceMedia_ ?
1256 isWaitingForIceAndMedia_ = (reinvIceMedia_ != nullptr);
1257
1258 JAMI_DEBUG("[call:{}] Set state to HOLD", getCallId());
1259 return true;
1260}
1261
1262bool
1263SIPCall::resume(OnReadyCb&& cb)
1264{
1265 // If ICE is currently negotiating, we must wait before attempting to resume the call
1266 if (isWaitingForIceAndMedia_) {
1267 JAMI_DEBUG("[call:{}] ICE negotiation in progress. Resume request will be once ICE "
1268 "negotiation completes",
1269 getCallId());
1270 resumeCb_ = std::move(cb);
1271 remainingRequest_ = Request::Resume;
1272 return false;
1273 }
1274 JAMI_DEBUG("[call:{}] Resuming the call", getCallId());
1275 auto result = resume();
1276
1277 if (cb)
1278 cb(result);
1279
1280 return result;
1281}
1282
1283bool
1284SIPCall::resume()
1285{
1286 auto account = getSIPAccount();
1287 if (!account) {
1288 JAMI_ERROR("[call:{}] No account detected", getCallId());
1289 return false;
1290 }
1291
1292 bool success = false;
1293 try {
1294 success = internalResume([] {});
1295 } catch (const SdpException& e) {
1296 JAMI_ERROR("[call:{}] {}", getCallId(), e.what());
1297 throw VoipLinkException("SDP issue in resume");
1298 }
1299
1300 // Only wait for ICE if we have an ICE re-invite in progress
1301 isWaitingForIceAndMedia_ = success and (reinvIceMedia_ != nullptr);
1302
1303 return success;
1304}
1305
1306bool
1307SIPCall::internalResume(const std::function<void()>& sdp_cb)
1308{
1309 if (getConnectionState() != ConnectionState::CONNECTED) {
1310 JAMI_WARNING("[call:{}] Not connected, ignoring resume request", getCallId());
1311 }
1312
1313 if (not setState(CallState::ACTIVE))
1314 return false;
1315
1316 sdp_cb();
1317
1318 {
1319 for (auto& stream : rtpStreams_) {
1320 stream.mediaAttribute_->hold_ = false;
1321 }
1322 // For now, call resume will always require new ICE negotiation.
1323 if (SIPSessionReinvite(getMediaAttributeList(), true) != PJ_SUCCESS) {
1324 JAMI_WARNING("[call:{}] Resuming hold", getCallId());
1325 if (isWaitingForIceAndMedia_) {
1326 remainingRequest_ = Request::Hold;
1327 } else {
1328 hold();
1329 }
1330 return false;
1331 }
1332 }
1333
1334 return true;
1335}
1336
1337void
1338SIPCall::switchInput(const std::string& source)
1339{
1340 JAMI_DEBUG("[call:{}] Set selected source to {}", getCallId(), source);
1341
1342 for (auto const& stream : rtpStreams_) {
1343 auto mediaAttr = stream.mediaAttribute_;
1344 mediaAttr->sourceUri_ = source;
1345 }
1346
1347 // Check if the call is being recorded in order to continue
1348 // … the recording after the switch
1349 bool isRec = Call::isRecording();
1350
1351 if (isWaitingForIceAndMedia_) {
1352 remainingRequest_ = Request::SwitchInput;
1353 } else {
1354 // For now, switchInput will always trigger a re-invite
1355 // with new ICE session.
1356 if (SIPSessionReinvite(getMediaAttributeList(), true) == PJ_SUCCESS and reinvIceMedia_) {
1357 isWaitingForIceAndMedia_ = true;
1358 }
1359 }
1360 if (isRec) {
1361 readyToRecord_ = false;
1362 pendingRecord_ = true;
1363 }
1364}
1365
1366void
1367SIPCall::peerHungup()
1368{
1369 pendingRecord_ = false;
1370 // Stop all RTP streams
1371 stopAllMedia();
1372
1373 if (inviteSession_)
1374 terminateSipSession(PJSIP_SC_NOT_FOUND);
1375 detachAudioFromConference();
1376 Call::peerHungup();
1377}
1378
1379void
1380SIPCall::carryingDTMFdigits(char code)
1381{
1382 int duration = Manager::instance().voipPreferences.getPulseLength();
1383 char dtmf_body[1000];
1384 int ret;
1385
1386 // handle flash code
1387 if (code == '!') {
1388 ret = snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=16\r\nDuration=%d\r\n", duration);
1389 } else {
1390 ret = snprintf(dtmf_body, sizeof dtmf_body - 1, "Signal=%c\r\nDuration=%d\r\n", code, duration);
1391 }
1392
1393 try {
1394 sendSIPInfo({dtmf_body, (size_t) ret}, "dtmf-relay");
1395 } catch (const std::exception& e) {
1396 JAMI_ERROR("[call:{}] Error sending DTMF: {}", getCallId(), e.what());
1397 }
1398}
1399
1400void
1401SIPCall::setVideoOrientation(int streamIdx, int rotation)
1402{
1403 std::string streamIdPart;
1404 if (streamIdx != -1)
1405 streamIdPart = fmt::format("<stream_id>{}</stream_id>", streamIdx);
1406 std::string sip_body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
1407 "<media_control><vc_primitive><to_encoder>"
1408 "<device_orientation="
1409 + std::to_string(-rotation) + "/>" + "</to_encoder>" + streamIdPart
1410 + "</vc_primitive></media_control>";
1411
1412 JAMI_DEBUG("[call:{}] Sending device orientation via SIP INFO {} for stream {}", getCallId(), rotation, streamIdx);
1413
1414 sendSIPInfo(sip_body, "media_control+xml");
1415}
1416
1417void
1418SIPCall::sendTextMessage(const std::map<std::string, std::string>& messages, const std::string& from)
1419{
1420 // TODO: for now we ignore the "from" (the previous implementation for sending this info was
1421 // buggy and verbose), another way to send the original message sender will be implemented
1422 // in the future
1423 if (not subcalls_.empty()) {
1424 pendingOutMessages_.emplace_back(messages, from);
1425 for (auto& c : subcalls_)
1426 c->sendTextMessage(messages, from);
1427 } else {
1428 if (inviteSession_) {
1429 try {
1430 // Ignore if the peer does not allow "MESSAGE" SIP method
1431 // NOTE:
1432 // The SIP "Allow" header is not mandatory as per RFC-3261. If it's
1433 // not present and since "MESSAGE" method is an extention method,
1434 // we choose to assume that the peer does not support the "MESSAGE"
1435 // method to prevent unexpected behavior when interoperating with
1436 // some SIP implementations.
1437 if (not isSipMethodAllowedByPeer(sip_utils::SIP_METHODS::MESSAGE)) {
1438 JAMI_WARNING("[call:{}] Peer does not allow \"{}\" method",
1439 getCallId(),
1440 sip_utils::SIP_METHODS::MESSAGE);
1441
1442 // Print peer's allowed methods
1443 JAMI_LOG("[call:{}] Peer's allowed methods: {}", getCallId(), peerAllowedMethods_);
1444 return;
1445 }
1446
1447 im::sendSipMessage(inviteSession_.get(), messages);
1448
1449 } catch (...) {
1450 JAMI_ERROR("[call:{}] Failed to send SIP text message", getCallId());
1451 }
1452 } else {
1453 pendingOutMessages_.emplace_back(messages, from);
1454 JAMI_ERROR("[call:{}] sendTextMessage: no invite session for this call", getCallId());
1455 }
1456 }
1457}
1458
1459void
1460SIPCall::removeCall(int code)
1461{
1462#ifdef ENABLE_PLUGIN
1463 jami::Manager::instance().getJamiPluginManager().getCallServicesManager().clearCallHandlerMaps(getCallId());
1464#endif
1465 std::lock_guard lk {callMutex_};
1466 JAMI_DEBUG("[call:{}] removeCall()", getCallId());
1467 if (sdp_) {
1468 sdp_->setActiveLocalSdpSession(nullptr);
1469 sdp_->setActiveRemoteSdpSession(nullptr);
1470 }
1471 Call::removeCall(code);
1472
1473 {
1474 std::lock_guard lk(transportMtx_);
1475 resetTransport(std::move(iceMedia_));
1476 resetTransport(std::move(reinvIceMedia_));
1477 }
1478
1479 setInviteSession();
1480 setSipTransport({});
1481}
1482
1483void
1484SIPCall::onFailure(int code)
1485{
1486 if (setState(CallState::MERROR, ConnectionState::DISCONNECTED, code)) {
1487 runOnMainThread([w = weak(), code] {
1488 if (auto shared = w.lock()) {
1489 auto& call = *shared;
1490 Manager::instance().callFailure(call);
1491 call.removeCall(code);
1492 }
1493 });
1494 }
1495}
1496
1497void
1498SIPCall::onBusyHere()
1499{
1500 if (getCallType() == CallType::OUTGOING)
1501 setState(CallState::PEER_BUSY, ConnectionState::DISCONNECTED);
1502 else
1503 setState(CallState::BUSY, ConnectionState::DISCONNECTED);
1504
1505 runOnMainThread([w = weak()] {
1506 if (auto shared = w.lock()) {
1507 auto& call = *shared;
1508 Manager::instance().callBusy(call);
1509 call.removeCall();
1510 }
1511 });
1512}
1513
1514void
1515SIPCall::onClosed()
1516{
1517 runOnMainThread([w = weak()] {
1518 if (auto shared = w.lock()) {
1519 auto& call = *shared;
1520 Manager::instance().peerHungupCall(call);
1521 call.removeCall();
1522 }
1523 });
1524}
1525
1526void
1527SIPCall::onAnswered()
1528{
1529 JAMI_WARNING("[call:{}] onAnswered()", getCallId());
1530 runOnMainThread([w = weak()] {
1531 if (auto shared = w.lock()) {
1532 if (shared->getConnectionState() != ConnectionState::CONNECTED) {
1533 shared->setState(CallState::ACTIVE, ConnectionState::CONNECTED);
1534 if (not shared->isSubcall()) {
1535 Manager::instance().peerAnsweredCall(*shared);
1536 }
1537 }
1538 }
1539 });
1540}
1541
1542void
1543SIPCall::sendKeyframe(int streamIdx)
1544{
1545#ifdef ENABLE_VIDEO
1546 dht::ThreadPool::computation().run([w = weak(), streamIdx] {
1547 if (auto sthis = w.lock()) {
1548 JAMI_DEBUG("[call:{}] Handling picture fast update request", sthis->getCallId());
1549 if (streamIdx == -1) {
1550 for (const auto& videoRtp : sthis->getRtpSessionList(MediaType::MEDIA_VIDEO))
1551 std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->forceKeyFrame();
1552 } else if (streamIdx > -1 && streamIdx < static_cast<int>(sthis->rtpStreams_.size())) {
1553 // Apply request for requested stream
1554 auto& stream = sthis->rtpStreams_[streamIdx];
1555 if (stream.rtpSession_ && stream.rtpSession_->getMediaType() == MediaType::MEDIA_VIDEO)
1556 std::static_pointer_cast<video::VideoRtpSession>(stream.rtpSession_)->forceKeyFrame();
1557 }
1558 }
1559 });
1560#endif
1561}
1562
1563bool
1564SIPCall::isIceEnabled() const
1565{
1566 return enableIce_;
1567}
1568
1569void
1570SIPCall::setPeerUaVersion(std::string_view ua)
1571{
1572 if (peerUserAgent_ == ua or ua.empty()) {
1573 // Silently ignore if it did not change or empty.
1574 return;
1575 }
1576
1577 if (peerUserAgent_.empty()) {
1578 JAMI_DEBUG("[call:{}] Set peer's User-Agent to [{}]", getCallId(), ua);
1579 } else if (not peerUserAgent_.empty()) {
1580 // Unlikely, but should be handled since we don't have control over the peer.
1581 // Even if it's unexpected, we still attempt to parse the UA version.
1582 JAMI_WARNING("[call:{}] Peer's User-Agent unexpectedly changed from [{}] to [{}]",
1583 getCallId(),
1584 peerUserAgent_,
1585 ua);
1586 }
1587
1588 peerUserAgent_ = ua;
1589
1590 // User-agent parsing
1591 constexpr std::string_view PACK_NAME(PACKAGE_NAME " ");
1592 auto pos = ua.find(PACK_NAME);
1593 if (pos == std::string_view::npos) {
1594 // Must have the expected package name.
1595 JAMI_WARNING("[call:{}] Unable to find the expected package name in peer's User-Agent", getCallId());
1596 return;
1597 }
1598
1599 ua = ua.substr(pos + PACK_NAME.length());
1600
1601 std::string_view version;
1602 // Unstable (un-released) versions has a hyphen + commit ID after
1603 // the version number. Find the commit ID if any, and ignore it.
1604 pos = ua.find('-');
1605 if (pos != std::string_view::npos) {
1606 // Get the version and ignore the commit ID.
1607 version = ua.substr(0, pos);
1608 } else {
1609 // Extract the version number.
1610 pos = ua.find(' ');
1611 if (pos != std::string_view::npos) {
1612 version = ua.substr(0, pos);
1613 }
1614 }
1615
1616 if (version.empty()) {
1617 JAMI_DEBUG("[call:{}] Unable to parse peer's version", getCallId());
1618 return;
1619 }
1620
1621 auto peerVersion = split_string_to_unsigned(version, '.');
1622 if (peerVersion.size() > 4u) {
1623 JAMI_WARNING("[call:{}] Unable to parse peer's version", getCallId());
1624 return;
1625 }
1626
1627 // Check if peer's version is at least 10.0.2 to enable multi-stream.
1628 peerSupportMultiStream_ = Account::meetMinimumRequiredVersion(peerVersion, MULTISTREAM_REQUIRED_VERSION);
1629 if (not peerSupportMultiStream_) {
1630 JAMI_DEBUG("Peer's version [{}] does not support multi-stream. "
1631 "Min required version: [{}]",
1632 version,
1634 }
1635
1636 // Check if peer's version is at least 13.11.0 to enable multi-audio-stream.
1637 peerSupportMultiAudioStream_ = Account::meetMinimumRequiredVersion(peerVersion, MULTIAUDIO_REQUIRED_VERSION);
1638 if (not peerSupportMultiAudioStream_) {
1639 JAMI_DEBUG("Peer's version [{}] does not support multi-audio-stream. "
1640 "Min required version: [{}]",
1641 version,
1643 }
1644
1645 // Check if peer's version is at least 13.3.0 to enable multi-ICE.
1646 peerSupportMultiIce_ = Account::meetMinimumRequiredVersion(peerVersion, MULTIICE_REQUIRED_VERSION);
1647 if (not peerSupportMultiIce_) {
1648 JAMI_DEBUG("Peer's version [{}] does not support more than 2 ICE media streams. "
1649 "Min required version: [{}]",
1650 version,
1652 }
1653
1654 // Check if peer's version supports re-invite without ICE renegotiation.
1655 peerSupportReuseIceInReinv_ = Account::meetMinimumRequiredVersion(peerVersion,
1657 if (not peerSupportReuseIceInReinv_) {
1658 JAMI_LOG("Peer's version [{:s}] does not support re-invite without ICE renegotiation. "
1659 "Min required version: [{:s}]",
1660 version,
1662 }
1663}
1664
1665void
1666SIPCall::setPeerAllowMethods(std::vector<std::string> methods)
1667{
1668 std::lock_guard lock {callMutex_};
1669 peerAllowedMethods_ = std::move(methods);
1670}
1671
1672bool
1673SIPCall::isSipMethodAllowedByPeer(const std::string_view method) const
1674{
1675 std::lock_guard lock {callMutex_};
1676
1677 return std::find(peerAllowedMethods_.begin(), peerAllowedMethods_.end(), method) != peerAllowedMethods_.end();
1678}
1679
1680void
1681SIPCall::onPeerRinging()
1682{
1683 JAMI_DEBUG("[call:{}] Peer ringing", getCallId());
1684 setState(ConnectionState::RINGING);
1685}
1686
1687void
1688SIPCall::addLocalIceAttributes()
1689{
1690 if (not isIceEnabled())
1691 return;
1692
1693 auto iceMedia = getIceMedia();
1694
1695 if (not iceMedia) {
1696 JAMI_ERROR("[call:{}] Invalid ICE instance", getCallId());
1697 return;
1698 }
1699
1700 auto start = std::chrono::steady_clock::now();
1701
1702 if (not iceMedia->isInitialized()) {
1703 JAMI_DEBUG("[call:{}] Waiting for ICE initialization", getCallId());
1704 // we need an initialized ICE to progress further
1705 if (iceMedia->waitForInitialization(DEFAULT_ICE_INIT_TIMEOUT) <= 0) {
1706 JAMI_ERROR("[call:{}] ICE initialization timed out", getCallId());
1707 return;
1708 }
1709 // ICE initialization may take longer than usual in some cases,
1710 // for instance when TURN servers do not respond in time (DNS
1711 // resolution or other issues).
1712 auto duration = std::chrono::steady_clock::now() - start;
1713 if (duration > EXPECTED_ICE_INIT_MAX_TIME) {
1714 JAMI_WARNING("[call:{:s}] ICE initialization time was unexpectedly high ({})",
1715 getCallId(),
1716 std::chrono::duration_cast<std::chrono::milliseconds>(duration));
1717 }
1718 }
1719
1720 // Check the state of ICE instance, the initialization may have failed.
1721 if (not iceMedia->isInitialized()) {
1722 JAMI_ERROR("[call:{}] ICE session is uninitialized", getCallId());
1723 return;
1724 }
1725
1726 // Check the state, the call might have been canceled while waiting.
1727 // for initialization.
1728 if (getState() == Call::CallState::OVER) {
1729 JAMI_WARNING("[call:{}] The call was terminated while waiting for ICE initialization", getCallId());
1730 return;
1731 }
1732
1733 auto account = getSIPAccount();
1734 if (not account) {
1735 JAMI_ERROR("[call:{}] No account detected", getCallId());
1736 return;
1737 }
1738 if (not sdp_) {
1739 JAMI_ERROR("[call:{}] No sdp detected", getCallId());
1740 return;
1741 }
1742
1743 JAMI_DEBUG("[call:{}] Add local attributes for ICE instance [{}]", getCallId(), fmt::ptr(iceMedia.get()));
1744
1745 sdp_->addIceAttributes(iceMedia->getLocalAttributes());
1746
1747 if (account->isIceCompIdRfc5245Compliant()) {
1748 unsigned streamIdx = 0;
1749 for (auto const& stream : rtpStreams_) {
1750 if (not stream.mediaAttribute_->enabled_) {
1751 // Dont add ICE candidates if the media is disabled
1752 JAMI_DEBUG("[call:{}] Media [{}] @ {} is disabled, don't add local candidates",
1753 getCallId(),
1754 stream.mediaAttribute_->toString(),
1755 streamIdx);
1756 continue;
1757 }
1758 JAMI_DEBUG("[call:{}] Add ICE local candidates for media [{}] @ {}",
1759 getCallId(),
1760 stream.mediaAttribute_->toString(),
1761 streamIdx);
1762 // RTP
1763 sdp_->addIceCandidates(streamIdx, iceMedia->getLocalCandidates(streamIdx, ICE_COMP_ID_RTP));
1764 // RTCP if it has its own port
1765 if (not rtcpMuxEnabled_) {
1766 sdp_->addIceCandidates(streamIdx, iceMedia->getLocalCandidates(streamIdx, ICE_COMP_ID_RTP + 1));
1767 }
1768
1769 streamIdx++;
1770 }
1771 } else {
1772 unsigned idx = 0;
1773 unsigned compId = 1;
1774 for (auto const& stream : rtpStreams_) {
1775 if (not stream.mediaAttribute_->enabled_) {
1776 // Skipping local ICE candidates if the media is disabled
1777 continue;
1778 }
1779 JAMI_DEBUG("[call:{}] Add ICE local candidates for media [{}] @ {}",
1780 getCallId(),
1781 stream.mediaAttribute_->toString(),
1782 idx);
1783 // RTP
1784 sdp_->addIceCandidates(idx, iceMedia->getLocalCandidates(compId));
1785 compId++;
1786
1787 // RTCP if it has its own port
1788 if (not rtcpMuxEnabled_) {
1789 sdp_->addIceCandidates(idx, iceMedia->getLocalCandidates(compId));
1790 compId++;
1791 }
1792
1793 idx++;
1794 }
1795 }
1796}
1797
1798std::vector<IceCandidate>
1799SIPCall::getAllRemoteCandidates(dhtnet::IceTransport& transport) const
1800{
1801 std::vector<IceCandidate> rem_candidates;
1802 for (unsigned mediaIdx = 0; mediaIdx < static_cast<unsigned>(rtpStreams_.size()); mediaIdx++) {
1803 IceCandidate cand;
1804 for (auto& line : sdp_->getIceCandidates(mediaIdx)) {
1805 if (transport.parseIceAttributeLine(mediaIdx, line, cand)) {
1806 JAMI_DEBUG("[call:{}] Add remote ICE candidate: {}", getCallId(), line);
1807 rem_candidates.emplace_back(std::move(cand));
1808 }
1809 }
1810 }
1811 return rem_candidates;
1812}
1813
1814std::shared_ptr<SystemCodecInfo>
1815SIPCall::getVideoCodec() const
1816{
1817#ifdef ENABLE_VIDEO
1818 // Return first video codec as we negotiate only one codec for the call
1819 // Note: with multistream we can negotiate codecs/stream, but it's not the case
1820 // in practice (same for audio), so just return the first video codec.
1821 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO))
1822 return videoRtp->getCodec();
1823#endif
1824 return {};
1825}
1826
1827std::shared_ptr<SystemCodecInfo>
1828SIPCall::getAudioCodec() const
1829{
1830 // Return first video codec as we negotiate only one codec for the call
1831 for (const auto& audioRtp : getRtpSessionList(MediaType::MEDIA_AUDIO))
1832 return audioRtp->getCodec();
1833 return {};
1834}
1835
1836void
1837SIPCall::addMediaStream(const MediaAttribute& mediaAttr)
1838{
1839 // Create and add the media stream with the provided attribute.
1840 // Do not create the RTP sessions yet.
1841 RtpStream stream;
1842 stream.mediaAttribute_ = std::make_shared<MediaAttribute>(mediaAttr);
1843
1844 // Set default media source if empty. Kept for backward compatibility.
1845#ifdef ENABLE_VIDEO
1846 if (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO && stream.mediaAttribute_->sourceUri_.empty()) {
1847 if (auto* videoManager = Manager::instance().getVideoManager()) {
1848 stream.mediaAttribute_->sourceUri_ = videoManager->videoDeviceMonitor.getMRLForDefaultDevice();
1849 }
1850 }
1851#endif
1852
1853 rtpStreams_.emplace_back(std::move(stream));
1854}
1855
1856size_t
1857SIPCall::initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
1858{
1859 for (size_t idx = 0; idx < mediaAttrList.size(); idx++) {
1860 auto const& mediaAttr = mediaAttrList.at(idx);
1861 if (mediaAttr.type_ != MEDIA_AUDIO && mediaAttr.type_ != MEDIA_VIDEO) {
1862 JAMI_ERROR("[call:{}] Unexpected media type {}", getCallId(), static_cast<int>(mediaAttr.type_));
1863 assert(false);
1864 }
1865
1866 addMediaStream(mediaAttr);
1867 auto& stream = rtpStreams_.back();
1868 try {
1869 createRtpSession(stream);
1870 JAMI_DEBUG("[call:{:s}] Added media @{:d}: {:s}", getCallId(), idx, stream.mediaAttribute_->toString(true));
1871 } catch (const std::exception& e) {
1872 JAMI_ERROR("[call:{:s}] Failed to create RTP session for media @{:d}: {:s}. Ignoring the media",
1873 getCallId(),
1874 idx,
1875 e.what());
1876 rtpStreams_.pop_back();
1877 }
1878 }
1879
1880 JAMI_DEBUG("[call:{:s}] Created {:d} media stream(s)", getCallId(), rtpStreams_.size());
1881
1882 return rtpStreams_.size();
1883}
1884
1885bool
1886SIPCall::hasVideo() const
1887{
1888#ifdef ENABLE_VIDEO
1889 std::function<bool(const RtpStream& stream)> videoCheck = [](auto const& stream) {
1890 bool validVideo = stream.mediaAttribute_ && stream.mediaAttribute_->hasValidVideo();
1891 bool validRemoteVideo = stream.remoteMediaAttribute_ && stream.remoteMediaAttribute_->hasValidVideo();
1892 return validVideo || validRemoteVideo;
1893 };
1894
1895 const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), videoCheck);
1896
1897 return iter != rtpStreams_.end();
1898#else
1899 return false;
1900#endif
1901}
1902
1903bool
1904SIPCall::isCaptureDeviceMuted(const MediaType& mediaType) const
1905{
1906 // Return true only if all media of type 'mediaType' that use capture devices
1907 // source, are muted.
1908 std::function<bool(const RtpStream& stream)> mutedCheck = [&mediaType](auto const& stream) {
1909 return (stream.mediaAttribute_->type_ == mediaType and not stream.mediaAttribute_->muted_);
1910 };
1911 const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck);
1912 return iter == rtpStreams_.end();
1913}
1914
1915void
1916SIPCall::setupNegotiatedMedia()
1917{
1918 JAMI_DEBUG("[call:{}] Updating negotiated media", getCallId());
1919
1920 if (not sipTransport_ or not sdp_) {
1921 JAMI_ERROR("[call:{}] Call is in an invalid state", getCallId());
1922 return;
1923 }
1924
1925 auto slots = sdp_->getMediaSlots();
1926 bool peer_hold {true};
1927 int streamIdx = -1;
1928
1929 for (const auto& slot : slots) {
1930 streamIdx++;
1931 const auto& local = slot.first;
1932 const auto& remote = slot.second;
1933
1934 // Skip disabled media
1935 if (not local.enabled) {
1936 JAMI_DEBUG("[call:{}] [SDP:slot#{}] The media is disabled, skipping", getCallId(), streamIdx);
1937 continue;
1938 }
1939
1940 if (static_cast<size_t>(streamIdx) >= rtpStreams_.size()) {
1941 throw std::runtime_error("Stream index is out-of-range");
1942 }
1943
1944 auto const& rtpStream = rtpStreams_[streamIdx];
1945
1946 if (not rtpStream.mediaAttribute_) {
1947 throw std::runtime_error("Missing media attribute");
1948 }
1949
1950 // To enable a media, it must be enabled on both sides.
1951 rtpStream.mediaAttribute_->enabled_ = local.enabled and remote.enabled;
1952
1953 if (not rtpStream.rtpSession_)
1954 throw std::runtime_error("Must have a valid RTP session");
1955
1956 if (local.type != MEDIA_AUDIO && local.type != MEDIA_VIDEO) {
1957 JAMI_ERROR("[call:{}] Unexpected media type {}", getCallId(), static_cast<int>(local.type));
1958 throw std::runtime_error("Invalid media attribute");
1959 }
1960
1961 if (local.type != remote.type) {
1962 JAMI_ERROR("[call:{}] [SDP:slot#{}] Inconsistent media type between local and remote",
1963 getCallId(),
1964 streamIdx);
1965 continue;
1966 }
1967
1968 if (local.enabled and not local.codec) {
1969 JAMI_WARNING("[call:{}] [SDP:slot#{}] Missing local codec", getCallId(), streamIdx);
1970 continue;
1971 }
1972
1973 if (remote.enabled and not remote.codec) {
1974 JAMI_WARNING("[call:{}] [SDP:slot#{}] Missing remote codec", getCallId(), streamIdx);
1975 continue;
1976 }
1977
1978 if (isSrtpEnabled() and local.enabled and not local.crypto) {
1979 JAMI_WARNING("[call:{}] [SDP:slot#{}] Secure mode but no local crypto attributes. "
1980 "Ignoring the media",
1981 getCallId(),
1982 streamIdx);
1983 continue;
1984 }
1985
1986 if (isSrtpEnabled() and remote.enabled and not remote.crypto) {
1987 JAMI_WARNING("[call:{}] [SDP:slot#{}] Secure mode but no crypto remote attributes. "
1988 "Ignoring the media",
1989 getCallId(),
1990 streamIdx);
1991 continue;
1992 }
1993
1994 // Aggregate hold info over all remote streams
1995 peer_hold &= remote.hold;
1996
1997 configureRtpSession(rtpStream.rtpSession_, rtpStream.mediaAttribute_, local, remote);
1998 }
1999
2000 // TODO. Do we really use this?
2001 if (not isSubcall() and peerHold_ != peer_hold) {
2002 peerHold_ = peer_hold;
2003 emitSignal<libjami::CallSignal::PeerHold>(getCallId(), peerHold_);
2004 }
2005}
2006
2007void
2008SIPCall::startAllMedia()
2009{
2010 JAMI_DEBUG("[call:{}] Starting all media", getCallId());
2011
2012 if (not sipTransport_ or not sdp_) {
2013 JAMI_ERROR("[call:{}] The call is in invalid state", getCallId());
2014 return;
2015 }
2016
2017 if (isSrtpEnabled() && not sipTransport_->isSecure()) {
2018 JAMI_WARNING("[call:{}] Crypto (SRTP) is negotiated over an insecure signaling transport", getCallId());
2019 }
2020
2021 // reset
2022 readyToRecord_ = false;
2023
2024 // Not restarting media loop on hold as it's a huge waste of CPU resources
2025 if (getState() != CallState::HOLD) {
2026 bool iceRunning = isIceRunning();
2027 auto remoteMediaList = Sdp::getMediaAttributeListFromSdp(sdp_->getActiveRemoteSdpSession());
2028 size_t idx = 0;
2029 for (auto& rtpStream : rtpStreams_) {
2030 if (not rtpStream.mediaAttribute_) {
2031 throw std::runtime_error("Missing media attribute");
2032 }
2033 if (idx >= remoteMediaList.size()) {
2034 JAMI_ERROR("[call:{}] Remote media list smaller than streams (idx={}, size={})",
2035 getCallId(),
2036 idx,
2037 remoteMediaList.size());
2038 break;
2039 }
2040 rtpStream.remoteMediaAttribute_ = std::make_shared<MediaAttribute>(remoteMediaList[idx]);
2041 if (not rtpStream.mediaAttribute_->enabled_) {
2042 JAMI_DEBUG("[call:{}] Skipping start for disabled stream @{}", getCallId(), idx);
2043 idx++;
2044 continue;
2045 }
2046 if (rtpStream.remoteMediaAttribute_->type_ == MediaType::MEDIA_VIDEO) {
2047 rtpStream.rtpSession_->setMuted(rtpStream.remoteMediaAttribute_->muted_, RtpSession::Direction::RECV);
2048 }
2049 dht::ThreadPool::io().run(
2050 [w = weak(),
2051 idx,
2052 isVideo = rtpStream.remoteMediaAttribute_->type_ == MediaType::MEDIA_VIDEO,
2053 iceRunning,
2054 rtpSession = rtpStream.rtpSession_,
2055 rtpSocketPair
2056 = std::make_shared<std::pair<std::unique_ptr<dhtnet::IceSocket>, std::unique_ptr<dhtnet::IceSocket>>>(
2057 std::move(rtpStream.rtpSocket_), std::move(rtpStream.rtcpSocket_))]() mutable {
2058 try {
2059 if (iceRunning) {
2060 rtpSession->start(std::move(rtpSocketPair->first), std::move(rtpSocketPair->second));
2061 } else {
2062 rtpSession->start(nullptr, nullptr);
2063 }
2064 if (isVideo) {
2065 if (auto call = w.lock())
2066 call->requestKeyframe(static_cast<int>(idx));
2067 }
2068#ifdef ENABLE_PLUGIN
2069 if (auto call = w.lock()) {
2070 // Create AVStreams associated with the call
2071 call->createCallAVStreams();
2072 }
2073#endif
2074 } catch (const std::exception& e) {
2075 JAMI_ERROR("[call:{}] Failed to start RTP session {}: {}",
2076 w.lock() ? w.lock()->getCallId() : "unknown",
2077 idx,
2078 e.what());
2079 }
2080 });
2081 idx++;
2082 }
2083 }
2084
2085 // Media is restarted, we can process the last hold request.
2086 isWaitingForIceAndMedia_ = false;
2087 if (remainingRequest_ != Request::NoRequest) {
2088 bool result = true;
2089 switch (remainingRequest_) {
2090 case Request::Hold:
2091 result = hold();
2092 if (holdCb_) {
2093 holdCb_(result);
2094 holdCb_ = nullptr;
2095 }
2096 break;
2097 case Request::Resume:
2098 result = resume();
2099 if (resumeCb_) {
2100 resumeCb_(result);
2101 resumeCb_ = nullptr;
2102 }
2103 break;
2104 case Request::SwitchInput:
2105 SIPSessionReinvite();
2106 break;
2107 default:
2108 break;
2109 }
2110 remainingRequest_ = Request::NoRequest;
2111 }
2112
2113 mediaRestartRequired_ = false;
2114}
2115
2116void
2117SIPCall::restartMediaSender()
2118{
2119 JAMI_DEBUG("[call:{}] Restarting TX media streams", getCallId());
2120 for (const auto& rtpSession : getRtpSessionList())
2121 rtpSession->restartSender();
2122}
2123
2124void
2125SIPCall::stopAllMedia()
2126{
2127 JAMI_DEBUG("[call:{}] Stopping all media", getCallId());
2128
2129#ifdef ENABLE_VIDEO
2130 {
2131 std::lock_guard lk(sinksMtx_);
2132 for (auto it = callSinksMap_.begin(); it != callSinksMap_.end();) {
2133 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO)) {
2134 auto& videoReceive = std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->getVideoReceive();
2135 if (videoReceive) {
2136 auto& sink = videoReceive->getSink();
2137 sink->detach(it->second.get());
2138 }
2139 }
2140 it->second->stop();
2141 it = callSinksMap_.erase(it);
2142 }
2143 }
2144#endif
2145 // Stop all RTP sessions in parallel
2146 std::mutex mtx;
2147 std::condition_variable cv;
2148 unsigned int stoppedCount = 0;
2149 unsigned int totalStreams = rtpStreams_.size();
2150
2151 for (const auto& rtpSession : getRtpSessionList()) {
2152 dht::ThreadPool::io().run([&, rtpSession]() {
2153 try {
2154 rtpSession->stop();
2155 } catch (const std::exception& e) {
2156 JAMI_ERROR("Failed to stop RTP session: {}", e.what());
2157 }
2158
2159 std::lock_guard lk(mtx);
2160 stoppedCount++;
2161 cv.notify_one();
2162 });
2163 }
2164
2165 // Wait for all streams to be stopped
2166 std::unique_lock lk(mtx);
2167 cv.wait(lk, [&] { return stoppedCount == totalStreams; });
2168
2169#ifdef ENABLE_PLUGIN
2170 {
2171 clearCallAVStreams();
2172 std::lock_guard lk(avStreamsMtx_);
2173 Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject(getCallId());
2174 }
2175#endif
2176}
2177
2178void
2179SIPCall::muteMedia(const std::string& mediaType, bool mute)
2180{
2181 auto type = MediaAttribute::stringToMediaType(mediaType);
2182
2183 if (type == MediaType::MEDIA_AUDIO) {
2184 JAMI_WARNING("[call:{}] {} all audio media", getCallId(), mute ? "muting " : "unmuting ");
2185
2186 } else if (type == MediaType::MEDIA_VIDEO) {
2187 JAMI_WARNING("[call:{}] {} all video media", getCallId(), mute ? "muting" : "unmuting");
2188 } else {
2189 JAMI_ERROR("[call:{}] Invalid media type {}", getCallId(), mediaType);
2190 assert(false);
2191 }
2192
2193 // Get the current media attributes.
2194 auto mediaList = getMediaAttributeList();
2195
2196 // Mute/Unmute all medias with matching type.
2197 for (auto& mediaAttr : mediaList) {
2198 if (mediaAttr.type_ == type) {
2199 mediaAttr.muted_ = mute;
2200 }
2201 }
2202
2203 // Apply
2204 requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList));
2205}
2206
2207void
2208SIPCall::updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx)
2209{
2210 assert(streamIdx < rtpStreams_.size());
2211
2212 auto const& rtpStream = rtpStreams_[streamIdx];
2213 assert(rtpStream.rtpSession_);
2214
2215 auto const& mediaAttr = rtpStream.mediaAttribute_;
2216 assert(mediaAttr);
2217
2218 bool notifyMute = false;
2219
2220 if (newMediaAttr.muted_ == mediaAttr->muted_) {
2221 // Nothing to do. Already in the desired state.
2222 JAMI_DEBUG("[call:{}] [{}] already {}",
2223 getCallId(),
2224 mediaAttr->label_,
2225 mediaAttr->muted_ ? "muted " : "unmuted ");
2226
2227 } else {
2228 // Update
2229 mediaAttr->muted_ = newMediaAttr.muted_;
2230 notifyMute = true;
2231 JAMI_DEBUG("[call:{}] {} [{}]", getCallId(), mediaAttr->muted_ ? "muting" : "unmuting", mediaAttr->label_);
2232 }
2233
2234 // Only update source and type if actually set.
2235 if (not newMediaAttr.sourceUri_.empty())
2236 mediaAttr->sourceUri_ = newMediaAttr.sourceUri_;
2237
2238 if (notifyMute and mediaAttr->type_ == MediaType::MEDIA_AUDIO) {
2239 rtpStream.rtpSession_->setMediaSource(mediaAttr->sourceUri_);
2240 rtpStream.rtpSession_->setMuted(mediaAttr->muted_);
2241 sendMuteState(mediaAttr->muted_);
2242 if (not isSubcall())
2243 emitSignal<libjami::CallSignal::AudioMuted>(getCallId(), mediaAttr->muted_);
2244 return;
2245 }
2246
2247#ifdef ENABLE_VIDEO
2248 if (notifyMute and mediaAttr->type_ == MediaType::MEDIA_VIDEO) {
2249 rtpStream.rtpSession_->setMediaSource(mediaAttr->sourceUri_);
2250 rtpStream.rtpSession_->setMuted(mediaAttr->muted_);
2251
2252 if (not isSubcall())
2253 emitSignal<libjami::CallSignal::VideoMuted>(getCallId(), mediaAttr->muted_);
2254 }
2255#endif
2256}
2257
2258bool
2259SIPCall::updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList, bool isRemote)
2260{
2261 JAMI_DEBUG("[call:{}] New local media", getCallId());
2262
2263 if (mediaAttrList.size() > PJ_ICE_MAX_COMP / 2) {
2264 JAMI_DEBUG("[call:{:s}] Too many media streams, limit it ({:d} vs {:d})",
2265 getCallId().c_str(),
2266 mediaAttrList.size(),
2267 PJ_ICE_MAX_COMP);
2268 return false;
2269 }
2270
2271 unsigned idx = 0;
2272 for (auto const& newMediaAttr : mediaAttrList) {
2273 JAMI_DEBUG("[call:{}] Media @{}: {}", getCallId(), idx++, newMediaAttr.toString(true));
2274 }
2275
2276 JAMI_DEBUG("[call:{}] Updating local media streams", getCallId());
2277
2278 for (auto const& newAttr : mediaAttrList) {
2279 auto streamIdx = findRtpStreamIndex(newAttr.label_);
2280
2281 if (streamIdx < 0) {
2282 // Media does not exist, add a new one.
2283 addMediaStream(newAttr);
2284 auto& stream = rtpStreams_.back();
2285 // If the remote asks for a new stream, our side sends nothing
2286 stream.mediaAttribute_->muted_ = isRemote ? true : stream.mediaAttribute_->muted_;
2287 try {
2288 createRtpSession(stream);
2289 JAMI_DEBUG("[call:{:s}] Added a new media stream @{:d}: {:s}",
2290 getCallId(),
2291 idx,
2292 stream.mediaAttribute_->toString(true));
2293 } catch (const std::exception& e) {
2294 JAMI_ERROR("[call:{:s}] Failed to create RTP session for media @{:d}: {:s}. Ignoring the media",
2295 getCallId(),
2296 idx,
2297 e.what());
2298 rtpStreams_.pop_back();
2299 }
2300 } else {
2301 updateMediaStream(newAttr, streamIdx);
2302 }
2303 }
2304
2305 // If the new media list is smaller than the number of existing streams, that means some streams have been removed
2306 // We need to clean them up
2307 if (mediaAttrList.size() < rtpStreams_.size()) {
2308#ifdef ENABLE_VIDEO
2309 for (auto i = mediaAttrList.size(); i < rtpStreams_.size(); ++i) {
2310 // Clean up video streams that are absent from the new media list
2311 auto& stream = rtpStreams_[i];
2312 if (stream.rtpSession_->getMediaType() == MediaType::MEDIA_VIDEO)
2313 std::static_pointer_cast<video::VideoRtpSession>(stream.rtpSession_)->exitConference();
2314 }
2315#endif
2316 for (auto i = mediaAttrList.size(); i < rtpStreams_.size(); ++i) {
2317 // Clean up audio streams that are absent from the new media list
2318 auto& stream = rtpStreams_[i];
2319 if (stream.rtpSession_->getMediaType() == MediaType::MEDIA_AUDIO) {
2320 JAMI_WARNING("[call:{}] Audio stream {} absent from new media list, stopping RTP session",
2321 getCallId(),
2322 stream.rtpSession_->streamId());
2323 std::static_pointer_cast<AudioRtpSession>(stream.rtpSession_)->stop();
2324 }
2325 }
2326 rtpStreams_.resize(mediaAttrList.size());
2327 }
2328 return true;
2329}
2330
2331bool
2332SIPCall::isReinviteRequired(const std::vector<MediaAttribute>& mediaAttrList)
2333{
2334 if (mediaAttrList.size() != rtpStreams_.size())
2335 return true;
2336
2337 for (auto const& newAttr : mediaAttrList) {
2338 auto streamIdx = findRtpStreamIndex(newAttr.label_);
2339
2340 if (streamIdx < 0) {
2341 // Always needs a re-invite when a new media is added.
2342 return true;
2343 }
2344
2345 // Changing the source needs a re-invite
2346 if (newAttr.sourceUri_ != rtpStreams_[streamIdx].mediaAttribute_->sourceUri_) {
2347 return true;
2348 }
2349
2350#ifdef ENABLE_VIDEO
2351 if (newAttr.type_ == MediaType::MEDIA_VIDEO) {
2352 // For now, only video mute triggers a re-invite.
2353 // Might be done for audio as well if required.
2354 if (newAttr.muted_ != rtpStreams_[streamIdx].mediaAttribute_->muted_) {
2355 return true;
2356 }
2357 }
2358#endif
2359 }
2360
2361 return false;
2362}
2363
2364bool
2365SIPCall::isNewIceMediaRequired(const std::vector<MediaAttribute>& mediaAttrList)
2366{
2367 // Always needs a new ICE media if the peer does not support
2368 // re-invite without ICE renegotiation
2369 if (not peerSupportReuseIceInReinv_)
2370 return true;
2371
2372 // Always needs a new ICE media when the number of media changes.
2373 if (mediaAttrList.size() != rtpStreams_.size())
2374 return true;
2375
2376 for (auto const& newAttr : mediaAttrList) {
2377 auto streamIdx = findRtpStreamIndex(newAttr.label_);
2378 if (streamIdx < 0) {
2379 // Always needs a new ICE media when a media is added or replaced.
2380 return true;
2381 }
2382 auto const& currAttr = rtpStreams_[streamIdx].mediaAttribute_;
2383 if (newAttr.sourceUri_ != currAttr->sourceUri_) {
2384 // For now, media will be restarted if the source changes.
2385 // TODO. This should not be needed if the decoder/receiver
2386 // correctly handles dynamic media properties changes.
2387 return true;
2388 }
2389
2390#ifdef ENABLE_VIDEO
2391 if (newAttr.type_ == MediaType::MEDIA_VIDEO) {
2392 // Video mute/unmute changes trigger a reinvite, and reinvites always clear ICE.
2393 // Therefore, we need recreate ICE transport.
2394 if (newAttr.muted_ != currAttr->muted_) {
2395 return true;
2396 }
2397 }
2398#endif
2399 }
2400
2401 return false;
2402}
2403
2404bool
2405SIPCall::requestMediaChange(const std::vector<libjami::MediaMap>& mediaList)
2406{
2407 std::lock_guard lk {callMutex_};
2408 auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
2409 bool hasFileSharing {false};
2410
2411 for (const auto& media : mediaAttrList) {
2412 if (!media.enabled_ || media.sourceUri_.empty())
2413 continue;
2414
2415 // Supported MRL schemes
2416 static const std::string sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
2417
2418 const auto pos = media.sourceUri_.find(sep);
2419 if (pos == std::string::npos)
2420 continue;
2421
2422 const auto prefix = media.sourceUri_.substr(0, pos);
2423 if ((pos + sep.size()) >= media.sourceUri_.size())
2424 continue;
2425
2427 hasFileSharing = true;
2428 mediaPlayerId_ = media.sourceUri_;
2429#ifdef ENABLE_VIDEO
2430 createMediaPlayer(mediaPlayerId_);
2431#endif
2432 }
2433 }
2434
2435 if (!hasFileSharing) {
2436#ifdef ENABLE_VIDEO
2437 closeMediaPlayer(mediaPlayerId_);
2438#endif
2439 mediaPlayerId_ = "";
2440 }
2441
2442 // Disable video if disabled in the account.
2443 auto account = getSIPAccount();
2444 if (not account) {
2445 JAMI_ERROR("[call:{}] No account detected", getCallId());
2446 return false;
2447 }
2448 if (not account->isVideoEnabled()) {
2449 for (auto& mediaAttr : mediaAttrList) {
2450 if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
2451 // This an API misuse. The new medialist should not contain video
2452 // if it was disabled in the account settings.
2453 JAMI_ERROR("[call:{}] New media has video, but it's disabled in the account. "
2454 "Ignoring the change request!",
2455 getCallId());
2456 return false;
2457 }
2458 }
2459 }
2460
2461 // If the peer does not support multi-stream and the size of the new
2462 // media list is different from the current media list, the media
2463 // change request will be ignored.
2464 if (not peerSupportMultiStream_ and rtpStreams_.size() != mediaAttrList.size()) {
2465 JAMI_WARNING("[call:{}] Peer does not support multi-stream. Media change request ignored", getCallId());
2466 return false;
2467 }
2468
2469 // If the peer does not support multi-audio-stream and the new
2470 // media list has more than one audio. Ignore the one that comes from a file.
2471 if (not peerSupportMultiAudioStream_ and rtpStreams_.size() != mediaAttrList.size() and hasFileSharing) {
2472 JAMI_WARNING("[call:{}] Peer does not support multi-audio-stream. New Audio will be ignored", getCallId());
2473 for (auto it = mediaAttrList.begin(); it != mediaAttrList.end();) {
2474 if (it->type_ == MediaType::MEDIA_AUDIO and !it->sourceUri_.empty() and mediaPlayerId_ == it->sourceUri_) {
2475 it = mediaAttrList.erase(it);
2476 continue;
2477 }
2478 ++it;
2479 }
2480 }
2481
2482 // If peer doesn't support multiple ice, keep only the last audio/video
2483 // This keep the old behaviour (if sharing both camera + sharing a file, will keep the shared file)
2484 if (!peerSupportMultiIce_) {
2485 if (mediaList.size() > 2)
2486 JAMI_WARNING("[call:{}] Peer does not support more than 2 ICE medias. "
2487 "Media change request modified",
2488 getCallId());
2489 MediaAttribute audioAttr(MediaType::MEDIA_AUDIO);
2490 MediaAttribute videoAttr;
2491 auto hasVideo = false, hasAudio = false;
2492 for (auto it = mediaAttrList.rbegin(); it != mediaAttrList.rend(); ++it) {
2493 if (it->type_ == MediaType::MEDIA_VIDEO && !hasVideo) {
2494 videoAttr = *it;
2495 videoAttr.label_ = sip_utils::DEFAULT_VIDEO_STREAMID;
2496 hasVideo = true;
2497 } else if (it->type_ == MediaType::MEDIA_AUDIO && !hasAudio) {
2498 audioAttr = *it;
2499 audioAttr.label_ = sip_utils::DEFAULT_AUDIO_STREAMID;
2500 hasAudio = true;
2501 }
2502 if (hasVideo && hasAudio)
2503 break;
2504 }
2505 mediaAttrList.clear();
2506 // Note: use the order VIDEO/AUDIO to avoid reinvite.
2507 // Note: always add at least one media for valid SDP (RFC4566)
2508 mediaAttrList.emplace_back(audioAttr);
2509 if (hasVideo)
2510 mediaAttrList.emplace_back(videoAttr);
2511 }
2512
2513 if (mediaAttrList.empty()) {
2514 JAMI_ERROR("[call:{}] Invalid media change request: new media list is empty", getCallId());
2515 return false;
2516 }
2517 JAMI_DEBUG("[call:{}] Requesting media change. List of new media:", getCallId());
2518
2519 unsigned idx = 0;
2520 for (auto const& newMediaAttr : mediaAttrList) {
2521 JAMI_DEBUG("[call:{}] Media @{:d}: {}", getCallId(), idx++, newMediaAttr.toString(true));
2522 }
2523
2524 auto needReinvite = isReinviteRequired(mediaAttrList);
2525 auto needNewIce = isNewIceMediaRequired(mediaAttrList);
2526
2527 if (!updateAllMediaStreams(mediaAttrList, false))
2528 return false;
2529
2530 if (needReinvite) {
2531 JAMI_DEBUG("[call:{}] Media change requires a new negotiation (re-invite)", getCallId());
2532 requestReinvite(mediaAttrList, needNewIce);
2533 } else {
2534 JAMI_DEBUG("[call:{}] Media change DOES NOT require a new negotiation (re-invite)", getCallId());
2535 reportMediaNegotiationStatus();
2536 }
2537
2538 return true;
2539}
2540
2541std::vector<std::map<std::string, std::string>>
2542SIPCall::currentMediaList() const
2543{
2544 return MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
2545}
2546
2547std::vector<MediaAttribute>
2548SIPCall::getMediaAttributeList() const
2549{
2550 std::lock_guard lk {callMutex_};
2551 std::vector<MediaAttribute> mediaList;
2552 mediaList.reserve(rtpStreams_.size());
2553 for (auto const& stream : rtpStreams_)
2554 mediaList.emplace_back(*stream.mediaAttribute_);
2555 return mediaList;
2556}
2557
2558std::map<std::string, bool>
2559SIPCall::getAudioStreams() const
2560{
2561 std::map<std::string, bool> audioMedias {};
2562 auto medias = getMediaAttributeList();
2563 for (const auto& media : medias) {
2564 if (media.type_ == MEDIA_AUDIO) {
2565 auto label = fmt::format("{}_{}", getCallId(), media.label_);
2566 audioMedias.emplace(label, media.muted_);
2567 }
2568 }
2569 return audioMedias;
2570}
2571
2572std::map<std::string, bool>
2573SIPCall::getRemoteAudioStreams() const
2574{
2575 std::lock_guard lk {callMutex_};
2576 std::map<std::string, bool> audioMedias {};
2577 for (const auto& stream : rtpStreams_) {
2578 if (stream.mediaAttribute_ && stream.mediaAttribute_->type_ == MEDIA_AUDIO) {
2579 auto label = fmt::format("{}_{}", getCallId(), stream.mediaAttribute_->label_);
2580 bool muted = stream.remoteMediaAttribute_ ? stream.remoteMediaAttribute_->muted_
2581 : stream.mediaAttribute_->muted_;
2582 audioMedias.emplace(label, muted);
2583 }
2584 }
2585 return audioMedias;
2586}
2587
2588void
2589SIPCall::onMediaNegotiationComplete()
2590{
2591 runOnMainThread([w = weak()] {
2592 if (auto this_ = w.lock()) {
2593 std::lock_guard lk {this_->callMutex_};
2594 JAMI_DEBUG("[call:{}] Media negotiation complete", this_->getCallId());
2595
2596 // If the call has already ended, we don't need to start the media.
2597 if (not this_->inviteSession_ or this_->inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED
2598 or not this_->sdp_) {
2599 return;
2600 }
2601
2602 // This method is called to report media negotiation (SDP) for initial
2603 // invite or subsequent invites (re-invite).
2604 // If ICE is negotiated, the media update will be handled in the
2605 // ICE callback, otherwise, it will be handled here.
2606 // Note that ICE can be negotiated in the first invite and not negotiated
2607 // in the re-invite. In this case, the media transport is unchanged (reused).
2608 if (this_->isIceEnabled() and this_->remoteHasValidIceAttributes()) {
2609 if (not this_->isSubcall()) {
2610 // Start ICE checks. Media will be started once ICE checks complete.
2611 this_->startIceMedia();
2612 }
2613 } else {
2614 // Update the negotiated media.
2615 if (this_->mediaRestartRequired_) {
2616 this_->setupNegotiatedMedia();
2617 // No ICE, start media now.
2618 JAMI_WARNING("[call:{}] ICE media disabled, using default media ports", this_->getCallId());
2619 // Start the media.
2620 this_->stopAllMedia();
2621 this_->startAllMedia();
2622 }
2623
2624 // this_->updateRemoteMedia();
2625 this_->reportMediaNegotiationStatus();
2626 }
2627 }
2628 });
2629}
2630
2631void
2632SIPCall::reportMediaNegotiationStatus()
2633{
2634 // Notify using the parent Id if it's a subcall.
2635 auto callId = isSubcall() ? parent_->getCallId() : getCallId();
2636 emitSignal<libjami::CallSignal::MediaNegotiationStatus>(
2638 std::lock_guard lk {mediaStateMutex_};
2639 auto previousState = isAudioOnly_;
2640 auto newState = !hasVideo();
2641
2642 if (!readyToRecord_) {
2643 return;
2644 }
2645
2646 if (previousState != newState && Call::isRecording()) {
2647 deinitRecorder();
2648 toggleRecording();
2649 pendingRecord_ = true;
2650 }
2651 isAudioOnly_ = newState;
2652
2653 if (pendingRecord_ && readyToRecord_) {
2654 toggleRecording();
2655 }
2656}
2657
2658void
2659SIPCall::startIceMedia()
2660{
2661 JAMI_DEBUG("[call:{}] Starting ICE", getCallId());
2662 auto iceMedia = getIceMedia();
2663 if (not iceMedia or iceMedia->isFailed()) {
2664 JAMI_ERROR("[call:{}] Media ICE init failed", getCallId());
2665 onFailure(PJSIP_SC_INTERNAL_SERVER_ERROR);
2666 return;
2667 }
2668
2669 if (iceMedia->isStarted()) {
2670 // NOTE: for incoming calls, the ICE is already there and running
2671 if (iceMedia->isRunning())
2672 onIceNegoSucceed();
2673 return;
2674 }
2675
2676 if (not iceMedia->isInitialized()) {
2677 // In this case, onInitDone will occurs after the startIceMedia
2678 waitForIceInit_ = true;
2679 return;
2680 }
2681
2682 // Start transport on SDP data and wait for negotiation
2683 if (!sdp_)
2684 return;
2685 auto rem_ice_attrs = sdp_->getIceAttributes();
2686 if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty()) {
2687 JAMI_ERROR("[call:{}] Missing remote media ICE attributes", getCallId());
2688 onFailure(PJSIP_SC_NOT_ACCEPTABLE_HERE);
2689 return;
2690 }
2691 if (not iceMedia->startIce(rem_ice_attrs, getAllRemoteCandidates(*iceMedia))) {
2692 JAMI_ERROR("[call:{}] ICE media failed to start", getCallId());
2693 onFailure(PJSIP_SC_NOT_ACCEPTABLE_HERE);
2694 }
2695}
2696
2697void
2698SIPCall::onIceNegoSucceed()
2699{
2700 std::lock_guard lk {callMutex_};
2701
2702 JAMI_DEBUG("[call:{}] ICE negotiation succeeded", getCallId());
2703
2704 // Check if the call is already ended, so we don't need to restart medias
2705 // This is typically the case in a multi-device context where one device
2706 // can stop a call. So do not start medias
2707 if (not inviteSession_ or inviteSession_->state == PJSIP_INV_STATE_DISCONNECTED or not sdp_) {
2708 JAMI_ERROR("[call:{}] ICE negotiation succeeded, but call is in invalid state", getCallId());
2709 return;
2710 }
2711
2712 // Update the negotiated media.
2713 setupNegotiatedMedia();
2714
2715 // If this callback is for a re-invite session then update
2716 // the ICE media transport.
2717 if (isIceEnabled())
2718 switchToIceReinviteIfNeeded();
2719
2720 for (unsigned int idx = 0, compId = 1; idx < rtpStreams_.size(); idx++, compId += 2) {
2721 // Create sockets for RTP and RTCP, and start the session.
2722 auto& rtpStream = rtpStreams_[idx];
2723 if (not rtpStream.mediaAttribute_ or not rtpStream.mediaAttribute_->enabled_) {
2724 JAMI_DEBUG("[call:{}] Skipping ICE socket for disabled stream @{}", getCallId(), idx);
2725 continue;
2726 }
2727 rtpStream.rtpSocket_ = newIceSocket(compId);
2728
2729 if (not rtcpMuxEnabled_) {
2730 rtpStream.rtcpSocket_ = newIceSocket(compId + 1);
2731 }
2732 }
2733
2734 // Start/Restart the media using the new transport
2735 stopAllMedia();
2736 startAllMedia();
2737 reportMediaNegotiationStatus();
2738}
2739
2740bool
2741SIPCall::checkMediaChangeRequest(const std::vector<libjami::MediaMap>& remoteMediaList)
2742{
2743 // The current media is considered to have changed if one of the
2744 // following condtions is true:
2745 //
2746 // - the number of media changed
2747 // - the type of one of the media changed (unlikely)
2748 // - one of the media was enabled/disabled
2749
2750 JAMI_DEBUG("[call:{}] Received a media change request", getCallId());
2751
2752 auto remoteMediaAttrList = MediaAttribute::buildMediaAttributesList(remoteMediaList, isSrtpEnabled());
2753 if (remoteMediaAttrList.size() != rtpStreams_.size())
2754 return true;
2755
2756 for (size_t i = 0; i < rtpStreams_.size(); i++) {
2757 if (remoteMediaAttrList[i].type_ != rtpStreams_[i].mediaAttribute_->type_)
2758 return true;
2759 if (remoteMediaAttrList[i].enabled_ != rtpStreams_[i].mediaAttribute_->enabled_)
2760 return true;
2761 }
2762
2763 return false;
2764}
2765
2766void
2767SIPCall::handleMediaChangeRequest(const std::vector<libjami::MediaMap>& remoteMediaList)
2768{
2769 JAMI_DEBUG("[call:{}] Handling media change request", getCallId());
2770
2771 auto account = getAccount().lock();
2772 if (not account) {
2773 JAMI_ERROR("No account detected");
2774 return;
2775 }
2776
2777 // If the offered media does not differ from the current local media, the
2778 // request is answered using the current local media.
2779 if (not checkMediaChangeRequest(remoteMediaList)) {
2780 answerMediaChangeRequest(MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()));
2781 return;
2782 }
2783
2784 if (account->isAutoAnswerEnabled()) {
2785 // NOTE:
2786 // Since the auto-answer is enabled in the account, newly
2787 // added media are accepted too.
2788 // This also means that if original call was an audio-only call,
2789 // the local camera will be enabled, unless the video is disabled
2790 // in the account settings.
2791
2792 std::vector<libjami::MediaMap> newMediaList;
2793 newMediaList.reserve(remoteMediaList.size());
2794 for (auto const& stream : rtpStreams_) {
2795 newMediaList.emplace_back(MediaAttribute::toMediaMap(*stream.mediaAttribute_));
2796 }
2797
2798 assert(remoteMediaList.size() > 0);
2799 if (remoteMediaList.size() > newMediaList.size()) {
2800 for (auto idx = newMediaList.size(); idx < remoteMediaList.size(); idx++) {
2801 newMediaList.emplace_back(remoteMediaList[idx]);
2802 }
2803 }
2804 answerMediaChangeRequest(newMediaList, true);
2805 return;
2806 }
2807
2808 // Report the media change request.
2809 emitSignal<libjami::CallSignal::MediaChangeRequested>(getAccountId(), getCallId(), remoteMediaList);
2810}
2811
2812pj_status_t
2813SIPCall::onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdata)
2814{
2815 JAMI_DEBUG("[call:{}] Received a re-invite", getCallId());
2816
2817 pj_status_t res = PJ_SUCCESS;
2818
2819 if (not sdp_) {
2820 JAMI_ERROR("SDP session is invalid");
2821 return res;
2822 }
2823
2824 sdp_->clearIce();
2825 sdp_->setActiveRemoteSdpSession(nullptr);
2826 sdp_->setActiveLocalSdpSession(nullptr);
2827
2828 auto acc = getSIPAccount();
2829 if (not acc) {
2830 JAMI_ERROR("No account detected");
2831 return res;
2832 }
2833
2834 Sdp::printSession(offer, "Remote session (media change request)", SdpDirection::OFFER);
2835
2836 sdp_->setReceivedOffer(offer);
2837
2838 // Note: For multistream, here we must ignore disabled remote medias, because
2839 // we will answer from our medias and remote enabled medias.
2840 // Example: if remote disables its camera and share its screen, the offer will
2841 // have an active and a disabled media (with port = 0).
2842 // In this case, if we have only one video, we can just negotiate 1 video instead of 2
2843 // with 1 disabled.
2844 // cf. pjmedia_sdp_neg_modify_local_offer2 for more details.
2845 auto const& mediaAttrList = Sdp::getMediaAttributeListFromSdp(offer, true);
2846 if (mediaAttrList.empty()) {
2847 JAMI_WARNING("[call:{}] Media list is empty, ignoring", getCallId());
2848 return res;
2849 }
2850
2851 if (upnp_) {
2852 openPortsUPnP();
2853 }
2854
2855 pjsip_tx_data* tdata = nullptr;
2856 if (pjsip_inv_initial_answer(inviteSession_.get(), rdata, PJSIP_SC_TRYING, NULL, NULL, &tdata) != PJ_SUCCESS) {
2857 JAMI_ERROR("[call:{}] Unable to create answer TRYING", getCallId());
2858 return res;
2859 }
2860
2861 dht::ThreadPool::io().run([callWkPtr = weak(), mediaAttrList] {
2862 if (auto call = callWkPtr.lock()) {
2863 // Report the change request.
2864 auto const& remoteMediaList = MediaAttribute::mediaAttributesToMediaMaps(mediaAttrList);
2865 if (auto conf = call->getConference()) {
2866 conf->handleMediaChangeRequest(call, remoteMediaList);
2867 } else {
2868 call->handleMediaChangeRequest(remoteMediaList);
2869 }
2870 }
2871 });
2872
2873 return res;
2874}
2875
2876void
2877SIPCall::onReceiveOfferIn200OK(const pjmedia_sdp_session* offer)
2878{
2879 if (not rtpStreams_.empty()) {
2880 JAMI_ERROR("[call:{}] Unexpected offer in '200 OK' answer", getCallId());
2881 return;
2882 }
2883
2884 auto acc = getSIPAccount();
2885 if (not acc) {
2886 JAMI_ERROR("No account detected");
2887 return;
2888 }
2889
2890 if (not sdp_) {
2891 JAMI_ERROR("Invalid SDP session");
2892 return;
2893 }
2894
2895 JAMI_DEBUG("[call:{}] Received an offer in '200 OK' answer", getCallId());
2896
2897 auto mediaList = Sdp::getMediaAttributeListFromSdp(offer);
2898 // If this method is called, it means we are expecting an offer
2899 // in the 200OK answer.
2900 if (mediaList.empty()) {
2901 JAMI_WARNING("[call:{}] Remote media list is empty, ignoring", getCallId());
2902 return;
2903 }
2904
2905 Sdp::printSession(offer, "Remote session (offer in 200 OK answer)", SdpDirection::OFFER);
2906
2907 sdp_->clearIce();
2908 sdp_->setActiveRemoteSdpSession(nullptr);
2909 sdp_->setActiveLocalSdpSession(nullptr);
2910
2911 sdp_->setReceivedOffer(offer);
2912
2913 // If we send an empty offer, video will be accepted only if locally
2914 // enabled by the user.
2915 for (auto& mediaAttr : mediaList) {
2916 if (mediaAttr.type_ == MediaType::MEDIA_VIDEO and not acc->isVideoEnabled()) {
2917 mediaAttr.enabled_ = false;
2918 }
2919 }
2920
2921 initMediaStreams(mediaList);
2922
2923 sdp_->processIncomingOffer(mediaList);
2924
2925 if (upnp_) {
2926 openPortsUPnP();
2927 }
2928
2929 if (isIceEnabled() and remoteHasValidIceAttributes()) {
2930 setupIceResponse();
2931 }
2932
2933 sdp_->startNegotiation();
2934
2935 if (pjsip_inv_set_sdp_answer(inviteSession_.get(), sdp_->getLocalSdpSession()) != PJ_SUCCESS) {
2936 JAMI_ERROR("[call:{}] Unable to start media negotiation for a re-invite request", getCallId());
2937 }
2938}
2939
2940void
2941SIPCall::openPortsUPnP()
2942{
2943 if (not sdp_) {
2944 JAMI_ERROR("[call:{}] Current SDP instance is invalid", getCallId());
2945 return;
2946 }
2947
2957 JAMI_DEBUG("[call:{}] Opening ports via UPnP for SDP session", getCallId());
2958
2959 // RTP port.
2960 upnp_->reserveMapping(sdp_->getLocalAudioPort(), dhtnet::upnp::PortType::UDP);
2961 // RTCP port.
2962 upnp_->reserveMapping(sdp_->getLocalAudioControlPort(), dhtnet::upnp::PortType::UDP);
2963
2964#ifdef ENABLE_VIDEO
2965 // RTP port.
2966 upnp_->reserveMapping(sdp_->getLocalVideoPort(), dhtnet::upnp::PortType::UDP);
2967 // RTCP port.
2968 upnp_->reserveMapping(sdp_->getLocalVideoControlPort(), dhtnet::upnp::PortType::UDP);
2969#endif
2970}
2971
2972std::map<std::string, std::string>
2973SIPCall::getDetails() const
2974{
2975 auto acc = getSIPAccount();
2976 if (!acc) {
2977 JAMI_ERROR("No account detected");
2978 return {};
2979 }
2980
2981 auto details = Call::getDetails();
2982
2983 details.emplace(libjami::Call::Details::PEER_HOLD, peerHold_ ? TRUE_STR : FALSE_STR);
2984
2985 for (auto const& stream : rtpStreams_) {
2986 if (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO) {
2987 details.emplace(libjami::Call::Details::VIDEO_SOURCE, stream.mediaAttribute_->sourceUri_);
2988#ifdef ENABLE_VIDEO
2989 if (auto const& rtpSession = stream.rtpSession_) {
2990 if (auto codec = rtpSession->getCodec()) {
2991 details.emplace(libjami::Call::Details::VIDEO_CODEC, codec->name);
2992 details.emplace(libjami::Call::Details::VIDEO_MIN_BITRATE, std::to_string(codec->minBitrate));
2993 details.emplace(libjami::Call::Details::VIDEO_MAX_BITRATE, std::to_string(codec->maxBitrate));
2994 if (const auto& curvideoRtpSession = std::static_pointer_cast<video::VideoRtpSession>(rtpSession)) {
2996 std::to_string(curvideoRtpSession->getVideoBitrateInfo().videoBitrateCurrent));
2997 }
2998 } else
2999 details.emplace(libjami::Call::Details::VIDEO_CODEC, "");
3000 }
3001#endif
3002 } else if (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO) {
3003 if (auto const& rtpSession = stream.rtpSession_) {
3004 if (auto codec = rtpSession->getCodec()) {
3005 details.emplace(libjami::Call::Details::AUDIO_CODEC, codec->name);
3006 details.emplace(
3008 codec->getCodecSpecifications()[libjami::Account::ConfProperties::CodecInfo::SAMPLE_RATE]);
3009 } else {
3010 details.emplace(libjami::Call::Details::AUDIO_CODEC, "");
3011 details.emplace(libjami::Call::Details::AUDIO_SAMPLE_RATE, "");
3012 }
3013 }
3014 }
3015 }
3016
3017 if (not peerRegisteredName_.empty())
3018 details.emplace(libjami::Call::Details::REGISTERED_NAME, peerRegisteredName_);
3019
3020#ifdef ENABLE_CLIENT_CERT
3021 std::lock_guard lk {callMutex_};
3022 if (transport_ and transport_->isSecure()) {
3023 const auto& tlsInfos = transport_->getTlsInfos();
3024 if (tlsInfos.cipher != PJ_TLS_UNKNOWN_CIPHER) {
3025 const auto& cipher = pj_ssl_cipher_name(tlsInfos.cipher);
3026 details.emplace(libjami::TlsTransport::TLS_CIPHER, cipher ? cipher : "");
3027 } else {
3028 details.emplace(libjami::TlsTransport::TLS_CIPHER, "");
3029 }
3030 if (tlsInfos.peerCert) {
3031 details.emplace(libjami::TlsTransport::TLS_PEER_CERT, tlsInfos.peerCert->toString());
3032 auto ca = tlsInfos.peerCert->issuer;
3033 unsigned n = 0;
3034 while (ca) {
3035 std::ostringstream name_str;
3036 name_str << libjami::TlsTransport::TLS_PEER_CA_ << n++;
3037 details.emplace(name_str.str(), ca->toString());
3038 ca = ca->issuer;
3039 }
3040 details.emplace(libjami::TlsTransport::TLS_PEER_CA_NUM, std::to_string(n));
3041 } else {
3042 details.emplace(libjami::TlsTransport::TLS_PEER_CERT, "");
3043 details.emplace(libjami::TlsTransport::TLS_PEER_CA_NUM, "");
3044 }
3045 }
3046#endif
3047 if (auto transport = getIceMedia()) {
3048 if (transport && transport->isRunning())
3049 details.emplace(libjami::Call::Details::SOCKETS, transport->link().c_str());
3050 }
3051 return details;
3052}
3053
3054void
3055SIPCall::enterConference(std::shared_ptr<Conference> conference)
3056{
3057 JAMI_DEBUG("[call:{}] Entering conference [{}]", getCallId(), conference->getConfId());
3058 conf_ = conference;
3059 // Unbind audio. It will be rebinded in the conference if needed
3060 auto const hasAudio = !getRtpSessionList(MediaType::MEDIA_AUDIO).empty();
3061 if (hasAudio) {
3062 auto& rbPool = Manager::instance().getRingBufferPool();
3063 auto medias = getAudioStreams();
3064 for (const auto& media : medias) {
3065 rbPool.unbindRingBuffers(media.first, RingBufferPool::DEFAULT_ID);
3066 }
3067 rbPool.flush(RingBufferPool::DEFAULT_ID);
3068 }
3069
3070#ifdef ENABLE_VIDEO
3071 if (conference->isVideoEnabled())
3072 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO))
3073 std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->enterConference(*conference);
3074#endif
3075
3076#ifdef ENABLE_PLUGIN
3077 clearCallAVStreams();
3078#endif
3079}
3080
3081void
3082SIPCall::exitConference()
3083{
3084 std::lock_guard lk {callMutex_};
3085 JAMI_DEBUG("[call:{}] Leaving conference", getCallId());
3086
3087 auto const hasAudio = !getRtpSessionList(MediaType::MEDIA_AUDIO).empty();
3088 if (hasAudio) {
3089 auto& rbPool = Manager::instance().getRingBufferPool();
3090 auto medias = getAudioStreams();
3091 for (const auto& media : medias) {
3092 if (!media.second) {
3093 rbPool.bindRingBuffers(media.first, RingBufferPool::DEFAULT_ID);
3094 }
3095 }
3096 rbPool.flush(RingBufferPool::DEFAULT_ID);
3097 }
3098#ifdef ENABLE_VIDEO
3099 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO))
3100 std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->exitConference();
3101#endif
3102#ifdef ENABLE_PLUGIN
3103 createCallAVStreams();
3104#endif
3105 conf_.reset();
3106}
3107
3108void
3109SIPCall::setActiveMediaStream(const std::string& accountUri,
3110 const std::string& deviceId,
3111 const std::string& streamId,
3112 const bool& state)
3113{
3114 auto remoteStreamId = streamId;
3115#ifdef ENABLE_VIDEO
3116 {
3117 std::lock_guard lk(sinksMtx_);
3118 const auto& localIt = local2RemoteSinks_.find(streamId);
3119 if (localIt != local2RemoteSinks_.end()) {
3120 remoteStreamId = localIt->second;
3121 }
3122 }
3123#endif
3124
3125 if (Call::conferenceProtocolVersion() == 1) {
3126 Json::Value sinkVal;
3127 sinkVal["active"] = state;
3128 Json::Value mediasObj;
3129 mediasObj[remoteStreamId] = sinkVal;
3130 Json::Value deviceVal;
3131 deviceVal["medias"] = mediasObj;
3132 Json::Value deviceObj;
3133 deviceObj[deviceId] = deviceVal;
3134 Json::Value accountVal;
3135 deviceVal["devices"] = deviceObj;
3136 Json::Value root;
3137 root[accountUri] = deviceVal;
3138 root["version"] = 1;
3139 Call::sendConfOrder(root);
3140 } else if (Call::conferenceProtocolVersion() == 0) {
3141 Json::Value root;
3142 root["activeParticipant"] = accountUri;
3143 Call::sendConfOrder(root);
3144 }
3145}
3146
3147#ifdef ENABLE_VIDEO
3148void
3149SIPCall::setRotation(int streamIdx, int rotation)
3150{
3151 // Retrigger on another thread to avoid to lock PJSIP
3152 dht::ThreadPool::io().run([w = weak(), streamIdx, rotation] {
3153 if (auto shared = w.lock()) {
3154 std::lock_guard lk {shared->callMutex_};
3155 shared->rotation_ = rotation;
3156 if (streamIdx == -1) {
3157 for (const auto& videoRtp : shared->getRtpSessionList(MediaType::MEDIA_VIDEO))
3158 std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->setRotation(rotation);
3159 } else if (streamIdx > -1 && streamIdx < static_cast<int>(shared->rtpStreams_.size())) {
3160 // Apply request for requested stream
3161 auto& stream = shared->rtpStreams_[streamIdx];
3162 if (stream.rtpSession_ && stream.rtpSession_->getMediaType() == MediaType::MEDIA_VIDEO)
3163 std::static_pointer_cast<video::VideoRtpSession>(stream.rtpSession_)->setRotation(rotation);
3164 }
3165 }
3166 });
3167}
3168
3169void
3170SIPCall::createSinks(ConfInfo& infos)
3171{
3172 std::lock_guard lk(callMutex_);
3173 std::lock_guard lkS(sinksMtx_);
3174 if (!hasVideo())
3175 return;
3176
3177 for (auto& participant : infos) {
3178 if (string_remove_suffix(participant.uri, '@') == account_.lock()->getUsername()
3179 && participant.device == std::dynamic_pointer_cast<JamiAccount>(account_.lock())->currentDeviceId()) {
3180 for (auto iter = rtpStreams_.begin(); iter != rtpStreams_.end(); iter++) {
3181 if (!iter->mediaAttribute_ || iter->mediaAttribute_->type_ == MediaType::MEDIA_AUDIO) {
3182 continue;
3183 }
3184 auto* localVideo
3185 = std::static_pointer_cast<video::VideoRtpSession>(iter->rtpSession_)->getVideoLocal().get();
3186 auto size = std::make_pair(10, 10);
3187 if (localVideo) {
3188 size = std::make_pair(localVideo->getWidth(), localVideo->getHeight());
3189 }
3190 const auto& mediaAttribute = iter->mediaAttribute_;
3191 if (participant.sinkId.find(mediaAttribute->label_) != std::string::npos) {
3192 local2RemoteSinks_[mediaAttribute->sourceUri_] = participant.sinkId;
3193 participant.sinkId = mediaAttribute->sourceUri_;
3194 participant.videoMuted = mediaAttribute->muted_;
3195 participant.w = size.first;
3196 participant.h = size.second;
3197 participant.x = 0;
3198 participant.y = 0;
3199 }
3200 }
3201 }
3202 }
3203
3204 std::vector<std::shared_ptr<video::VideoFrameActiveWriter>> sinks;
3205 for (const auto& videoRtp : getRtpSessionList(MediaType::MEDIA_VIDEO)) {
3206 auto& videoReceive = std::static_pointer_cast<video::VideoRtpSession>(videoRtp)->getVideoReceive();
3207 if (!videoReceive)
3208 continue;
3209 sinks.emplace_back(std::static_pointer_cast<video::VideoFrameActiveWriter>(videoReceive->getSink()));
3210 }
3211 auto conf = conf_.lock();
3212 const auto& id = conf ? conf->getConfId() : getCallId();
3213 Manager::instance().createSinkClients(id, infos, sinks, callSinksMap_, getAccountId());
3214}
3215#endif
3216
3217std::vector<std::shared_ptr<RtpSession>>
3218SIPCall::getRtpSessionList(MediaType type) const
3219{
3220 std::vector<std::shared_ptr<RtpSession>> rtpList;
3221 rtpList.reserve(rtpStreams_.size());
3222 for (auto const& stream : rtpStreams_) {
3223 if (type == MediaType::MEDIA_ALL || stream.rtpSession_->getMediaType() == type)
3224 rtpList.emplace_back(stream.rtpSession_);
3225 }
3226 return rtpList;
3227}
3228
3229void
3230SIPCall::monitor() const
3231{
3232 if (isSubcall())
3233 return;
3234 auto acc = getSIPAccount();
3235 if (!acc) {
3236 JAMI_ERROR("No account detected");
3237 return;
3238 }
3239 JAMI_LOG("- Call {} with {}:", getCallId(), getPeerNumber());
3240 JAMI_LOG("\t- Duration: {}", dht::print_duration(getCallDuration()));
3241 for (const auto& stream : rtpStreams_)
3242 JAMI_LOG("\t- Media: {}", stream.mediaAttribute_->toString(true));
3243#ifdef ENABLE_VIDEO
3244 if (auto codec = getVideoCodec())
3245 JAMI_LOG("\t- Video codec: {}", codec->name);
3246#endif
3247 if (auto transport = getIceMedia()) {
3248 if (transport->isRunning())
3249 JAMI_LOG("\t- Media stream(s): {}", transport->link());
3250 }
3251}
3252
3253bool
3254SIPCall::toggleRecording()
3255{
3256 pendingRecord_ = true;
3257 if (not readyToRecord_)
3258 return true;
3259
3260 // add streams to recorder before starting the record
3261 if (not Call::isRecording()) {
3262 auto account = getSIPAccount();
3263 if (!account) {
3264 JAMI_ERROR("No account detected");
3265 return false;
3266 }
3267 auto title = fmt::format("Conversation at %TIMESTAMP between {} and {}", account->getUserUri(), peerUri_);
3268 recorder_->setMetadata(title, ""); // use default description
3269 for (const auto& rtpSession : getRtpSessionList())
3270 rtpSession->initRecorder();
3271 } else {
3272 updateRecState(false);
3273 }
3274 pendingRecord_ = false;
3275 auto state = Call::toggleRecording();
3276 if (state)
3277 updateRecState(state);
3278 return state;
3279}
3280
3281void
3282SIPCall::deinitRecorder()
3283{
3284 for (const auto& rtpSession : getRtpSessionList())
3285 rtpSession->deinitRecorder();
3286}
3287
3288void
3289SIPCall::InvSessionDeleter::operator()(pjsip_inv_session* inv) const noexcept
3290{
3291 // prevent this from getting accessed in callbacks
3292 // JAMI_WARN: this is not thread-safe!
3293 if (!inv)
3294 return;
3295 inv->mod_data[Manager::instance().sipVoIPLink().getModId()] = nullptr;
3296 // NOTE: the counter is incremented by sipvoiplink (transaction_request_cb)
3297 pjsip_inv_dec_ref(inv);
3298}
3299
3300bool
3301SIPCall::createIceMediaTransport(bool isReinvite)
3302{
3303 auto mediaTransport = Manager::instance().getIceTransportFactory()->createTransport(getCallId());
3304 if (mediaTransport) {
3305 JAMI_DEBUG("[call:{}] Successfully created media ICE transport [ice:{}]",
3306 getCallId(),
3307 fmt::ptr(mediaTransport.get()));
3308 } else {
3309 JAMI_ERROR("[call:{}] Failed to create media ICE transport", getCallId());
3310 return {};
3311 }
3312
3313 setIceMedia(mediaTransport, isReinvite);
3314
3315 return mediaTransport != nullptr;
3316}
3317
3318bool
3319SIPCall::initIceMediaTransport(bool master, std::optional<dhtnet::IceTransportOptions> options)
3320{
3321 auto acc = getSIPAccount();
3322 if (!acc) {
3323 JAMI_ERROR("No account detected");
3324 return false;
3325 }
3326
3327 JAMI_DEBUG("[call:{}] Init media ICE transport", getCallId());
3328
3329 auto const& iceMedia = getIceMedia();
3330 if (not iceMedia) {
3331 JAMI_ERROR("[call:{}] Invalid media ICE transport", getCallId());
3332 return false;
3333 }
3334
3335 auto iceOptions = options == std::nullopt ? acc->getIceOptions() : *options;
3336
3337 auto optOnInitDone = std::move(iceOptions.onInitDone);
3338 auto optOnNegoDone = std::move(iceOptions.onNegoDone);
3339 iceOptions.onInitDone = [w = weak(), cb = std::move(optOnInitDone)](bool ok) {
3340 runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
3341 auto call = w.lock();
3342 if (cb)
3343 cb(ok);
3344 if (!ok or !call or !call->waitForIceInit_.exchange(false))
3345 return;
3346
3347 std::lock_guard lk {call->callMutex_};
3348 auto rem_ice_attrs = call->sdp_->getIceAttributes();
3349 // Init done but no remote_ice_attributes, the ice->start will be triggered later
3350 if (rem_ice_attrs.ufrag.empty() or rem_ice_attrs.pwd.empty())
3351 return;
3352 call->startIceMedia();
3353 });
3354 };
3355 iceOptions.onNegoDone = [w = weak(), cb = std::move(optOnNegoDone)](bool ok) {
3356 runOnMainThread([w = std::move(w), cb = std::move(cb), ok] {
3357 if (cb)
3358 cb(ok);
3359 if (auto call = w.lock()) {
3360 // The ICE is related to subcalls, but medias are handled by parent call
3361 std::lock_guard lk {call->callMutex_};
3362 call = call->isSubcall() ? std::dynamic_pointer_cast<SIPCall>(call->parent_) : call;
3363 if (!ok) {
3364 JAMI_ERROR("[call:{}] Media ICE negotiation failed", call->getCallId());
3365 call->onFailure(PJSIP_SC_NOT_ACCEPTABLE_HERE);
3366 return;
3367 }
3368 call->onIceNegoSucceed();
3369 }
3370 });
3371 };
3372
3373 iceOptions.master = master;
3374 iceOptions.streamsCount = static_cast<unsigned>(rtpStreams_.size());
3375 // Each RTP stream requires a pair of ICE components (RTP + RTCP).
3376 iceOptions.compCountPerStream = ICE_COMP_COUNT_PER_STREAM;
3377 iceOptions.qosType.reserve(rtpStreams_.size() * ICE_COMP_COUNT_PER_STREAM);
3378 for (const auto& stream : rtpStreams_) {
3379 iceOptions.qosType.push_back(stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO ? dhtnet::QosType::VOICE
3380 : dhtnet::QosType::VIDEO);
3381 iceOptions.qosType.push_back(dhtnet::QosType::CONTROL);
3382 }
3383
3384 // Init ICE.
3385 iceMedia->initIceInstance(iceOptions);
3386
3387 return true;
3388}
3389
3390std::vector<std::string>
3391SIPCall::getLocalIceCandidates(unsigned compId) const
3392{
3393 std::lock_guard lk(transportMtx_);
3394 if (not iceMedia_) {
3395 JAMI_WARNING("[call:{}] No media ICE transport", getCallId());
3396 return {};
3397 }
3398 return iceMedia_->getLocalCandidates(compId);
3399}
3400
3401void
3402SIPCall::resetTransport(std::shared_ptr<dhtnet::IceTransport>&& transport)
3403{
3404 // Move the transport to another thread and destroy it there if possible
3405 if (transport) {
3406 dht::ThreadPool::io().run([transport = std::move(transport)]() mutable { transport.reset(); });
3407 }
3408}
3409
3410void
3411SIPCall::merge(Call& call)
3412{
3413 JAMI_DEBUG("[call:{}] Merge subcall {}", getCallId(), call.getCallId());
3414
3415 // This static cast is safe as this method is private and overload Call::merge
3416 auto& subcall = static_cast<SIPCall&>(call);
3417
3418 std::lock(callMutex_, subcall.callMutex_);
3419 std::lock_guard lk1 {callMutex_, std::adopt_lock};
3420 std::lock_guard lk2 {subcall.callMutex_, std::adopt_lock};
3421 inviteSession_ = std::move(subcall.inviteSession_);
3422 if (inviteSession_)
3423 inviteSession_->mod_data[Manager::instance().sipVoIPLink().getModId()] = this;
3424 setSipTransport(std::move(subcall.sipTransport_), std::move(subcall.contactHeader_));
3425 sdp_ = std::move(subcall.sdp_);
3426 peerHold_ = subcall.peerHold_;
3427 upnp_ = std::move(subcall.upnp_);
3428 localAudioPort_ = subcall.localAudioPort_;
3429 localVideoPort_ = subcall.localVideoPort_;
3430 peerUserAgent_ = subcall.peerUserAgent_;
3431 peerSupportMultiStream_ = subcall.peerSupportMultiStream_;
3432 peerSupportMultiAudioStream_ = subcall.peerSupportMultiAudioStream_;
3433 peerSupportMultiIce_ = subcall.peerSupportMultiIce_;
3434 peerAllowedMethods_ = subcall.peerAllowedMethods_;
3435 peerSupportReuseIceInReinv_ = subcall.peerSupportReuseIceInReinv_;
3436
3437 Call::merge(subcall);
3438 if (isIceEnabled())
3439 startIceMedia();
3440}
3441
3442bool
3443SIPCall::remoteHasValidIceAttributes() const
3444{
3445 if (not sdp_) {
3446 throw std::runtime_error("Must have a valid SDP Session");
3447 }
3448
3449 auto rem_ice_attrs = sdp_->getIceAttributes();
3450 if (rem_ice_attrs.ufrag.empty()) {
3451 JAMI_DEBUG("[call:{}] No ICE username fragment attribute in remote SDP", getCallId());
3452 return false;
3453 }
3454
3455 if (rem_ice_attrs.pwd.empty()) {
3456 JAMI_DEBUG("[call:{}] No ICE password attribute in remote SDP", getCallId());
3457 return false;
3458 }
3459
3460 return true;
3461}
3462
3463void
3464SIPCall::setIceMedia(std::shared_ptr<dhtnet::IceTransport> ice, bool isReinvite)
3465{
3466 std::lock_guard lk(transportMtx_);
3467
3468 if (isReinvite) {
3469 JAMI_DEBUG("[call:{}] Setting re-invite ICE session [{}]", getCallId(), fmt::ptr(ice.get()));
3470 resetTransport(std::move(reinvIceMedia_));
3471 reinvIceMedia_ = std::move(ice);
3472 } else {
3473 JAMI_DEBUG("[call:{}] Setting ICE session [{}]", getCallId(), fmt::ptr(ice.get()));
3474 resetTransport(std::move(iceMedia_));
3475 iceMedia_ = std::move(ice);
3476 }
3477}
3478
3479void
3480SIPCall::switchToIceReinviteIfNeeded()
3481{
3482 std::lock_guard lk(transportMtx_);
3483
3484 if (reinvIceMedia_) {
3485 JAMI_DEBUG("[call:{}] Switching to re-invite ICE session [{}]", getCallId(), fmt::ptr(reinvIceMedia_.get()));
3486 std::swap(reinvIceMedia_, iceMedia_);
3487 }
3488
3489 resetTransport(std::move(reinvIceMedia_));
3490}
3491
3492void
3493SIPCall::setupIceResponse(bool isReinvite)
3494{
3495 JAMI_DEBUG("[call:{}] Setup ICE response", getCallId());
3496
3497 auto account = getSIPAccount();
3498 if (not account) {
3499 JAMI_ERROR("No account detected");
3500 }
3501
3502 auto opt = account->getIceOptions();
3503
3504 // Attempt to use the discovered public address. If not available,
3505 // fallback on local address.
3506 opt.accountPublicAddr = account->getPublishedIpAddress();
3507 if (opt.accountPublicAddr) {
3508 opt.accountLocalAddr = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(),
3509 opt.accountPublicAddr.getFamily());
3510 } else {
3511 // Just set the local address for both, most likely the account is not
3512 // registered.
3513 opt.accountLocalAddr = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(), AF_INET);
3514 opt.accountPublicAddr = opt.accountLocalAddr;
3515 }
3516
3517 if (not opt.accountLocalAddr) {
3518 JAMI_ERROR("[call:{}] No local address, unable to initialize ICE", getCallId());
3519 onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
3520 return;
3521 }
3522
3523 if (not createIceMediaTransport(isReinvite) or not initIceMediaTransport(false, opt)) {
3524 JAMI_ERROR("[call:{}] ICE initialization failed", getCallId());
3525 // Fatal condition
3526 // TODO: what's SIP rfc says about that?
3527 // (same question in startIceMedia)
3528 onFailure(PJSIP_SC_INTERNAL_SERVER_ERROR);
3529 return;
3530 }
3531
3532 // Media transport changed, must restart the media.
3533 mediaRestartRequired_ = true;
3534
3535 // WARNING: This call blocks! (need ICE init done)
3536 addLocalIceAttributes();
3537}
3538
3539bool
3540SIPCall::isIceRunning() const
3541{
3542 std::lock_guard lk(transportMtx_);
3543 return iceMedia_ and iceMedia_->isRunning();
3544}
3545
3546std::unique_ptr<dhtnet::IceSocket>
3547SIPCall::newIceSocket(unsigned compId)
3548{
3549 return std::unique_ptr<dhtnet::IceSocket> {new dhtnet::IceSocket(getIceMedia(), static_cast<int>(compId))};
3550}
3551
3552void
3553SIPCall::rtpSetupSuccess()
3554{
3555 std::lock_guard lk {mediaStateMutex_};
3556
3557 readyToRecord_ = true; // We're ready to record whenever a stream is ready
3558
3559 auto previousState = isAudioOnly_;
3560 auto newState = !hasVideo();
3561
3562 if (previousState != newState && Call::isRecording()) {
3563 deinitRecorder();
3565 pendingRecord_ = true;
3566 }
3567 isAudioOnly_ = newState;
3568
3569 if (pendingRecord_ && readyToRecord_)
3571}
3572
3573void
3574SIPCall::peerRecording(bool state)
3575{
3576 auto conference = conf_.lock();
3577 const std::string& id = conference ? conference->getConfId() : getCallId();
3578 if (state) {
3579 JAMI_WARNING("[call:{}] Peer is recording", getCallId());
3580 emitSignal<libjami::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), true);
3581 } else {
3582 JAMI_WARNING("Peer stopped recording");
3583 emitSignal<libjami::CallSignal::RemoteRecordingChanged>(id, getPeerNumber(), false);
3584 }
3585 peerRecording_ = state;
3586 if (auto conf = conf_.lock())
3587 conf->updateRecording();
3588}
3589
3590void
3591SIPCall::peerMuted(bool muted, int streamIdx)
3592{
3593 if (muted) {
3594 JAMI_WARNING("Peer muted");
3595 } else {
3596 JAMI_WARNING("Peer unmuted");
3597 }
3598
3599 if (streamIdx == -1) {
3600 for (const auto& audioRtp : getRtpSessionList(MediaType::MEDIA_AUDIO))
3601 audioRtp->setMuted(muted, RtpSession::Direction::RECV);
3602 } else if (streamIdx > -1 && streamIdx < static_cast<int>(rtpStreams_.size())) {
3603 auto& stream = rtpStreams_[streamIdx];
3604 if (stream.rtpSession_ && stream.rtpSession_->getMediaType() == MediaType::MEDIA_AUDIO)
3605 stream.rtpSession_->setMuted(muted, RtpSession::Direction::RECV);
3606 }
3607
3608 peerMuted_ = muted;
3609 if (auto conf = conf_.lock())
3610 conf->updateMuted();
3611}
3612
3613void
3614SIPCall::peerVoice(bool voice)
3615{
3616 peerVoice_ = voice;
3617
3618 if (auto conference = conf_.lock()) {
3619 conference->updateVoiceActivity();
3620 } else {
3621 // one-to-one call
3622 // maybe emit signal with partner voice activity
3623 }
3624}
3625
3626} // namespace jami
const std::string & getCallId() const
Return a reference on the call id.
Definition call.h:111
CallState getState() const
Get the call state of the call (protected by mutex)
Definition call.cpp:160
std::function< void(bool)> OnReadyCb
Definition call.h:94
CallType type_
Type of the call.
Definition call.h:491
CallType
This determines if the call originated from the local user (OUTGOING) or from some remote peer (INCOM...
Definition call.h:101
const std::string id_
MultiDevice: parent call, nullptr otherwise. Access protected by callMutex_.
Definition call.h:453
std::recursive_mutex callMutex_
Protect every attribute that can be changed by two threads.
Definition call.h:467
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:694
static std::vector< MediaAttribute > buildMediaAttributesList(const std::vector< libjami::MediaMap > &mediaList, bool secure)
std::string toString(bool full=false) const
static char const * mediaTypeToString(MediaType type)
std::shared_ptr< MediaRecorder > recorder_
Definition recordable.h:69
void terminateSipSession(int status)
Definition sipcall.cpp:718
SIPCall(const std::shared_ptr< SIPAccountBase > &account, const std::string &id, Call::CallType type, const std::vector< libjami::MediaMap > &mediaList)
Constructor.
Definition sipcall.cpp:97
bool isSrtpEnabled() const
Definition sipcall.h:154
void setSipTransport(const std::shared_ptr< SipTransport > &transport, const std::string &contactHdr={})
Definition sipcall.cpp:429
~SIPCall()
Destructor.
Definition sipcall.cpp:142
std::shared_ptr< SIPAccountBase > getSIPAccount() const
Definition sipcall.cpp:301
std::unique_ptr< pjsip_inv_session, InvSessionDeleter > inviteSession_
Definition sipcall.h:297
std::weak_ptr< const SIPCall > weak() const
Definition sipcall.h:299
void setInviteSession(pjsip_inv_session *inviteSession=nullptr)
Definition sipcall.cpp:689
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:238
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
#define JAMI_LOG(formatstr,...)
Definition logger.h:237
void setState(const std::string &accountID, const State migrationState)
std::string streamId(const std::string &callId, std::string_view label)
constexpr const pj_str_t CONST_PJ_STR(T(&a)[N]) noexcept
Definition sip_utils.h:87
static const std::vector< unsigned > REUSE_ICE_IN_REINVITE_REQUIRED_VERSION
Definition sipcall.cpp:92
static const std::vector< unsigned > MULTIICE_REQUIRED_VERSION
Definition sipcall.cpp:85
static constexpr auto MULTIAUDIO_REQUIRED_VERSION_STR
Definition sipcall.cpp:93
static constexpr int ICE_COMP_ID_RTP
Definition sipcall.cpp:79
bool closeMediaPlayer(const std::string &id)
void emitSignal(Args... args)
Definition jami_signal.h:64
static constexpr std::chrono::milliseconds MS_BETWEEN_2_KEYFRAME_REQUEST
Definition sipcall.cpp:78
static constexpr std::chrono::milliseconds EXPECTED_ICE_INIT_MAX_TIME
Definition sipcall.cpp:77
pj_ice_sess_cand IceCandidate
Definition sipcall.h:65
static const std::vector< unsigned > MULTIAUDIO_REQUIRED_VERSION
Definition sipcall.cpp:95
static const std::vector< unsigned > MULTISTREAM_REQUIRED_VERSION
Definition sipcall.cpp:83
std::vector< unsigned > split_string_to_unsigned(std::string_view str, char delim)
static const std::vector< unsigned > NEW_CONFPROTOCOL_VERSION
Definition sipcall.cpp:88
std::string_view string_remove_suffix(std::string_view str, char separator)
static constexpr auto NEW_CONFPROTOCOL_VERSION_STR
Definition sipcall.cpp:87
static void transfer_client_cb(pjsip_evsub *sub, pjsip_event *event)
Definition sipcall.cpp:1049
static constexpr std::chrono::seconds DEFAULT_ICE_INIT_TIMEOUT
Definition sipcall.cpp:76
static constexpr auto REUSE_ICE_IN_REINVITE_REQUIRED_VERSION_STR
Definition sipcall.cpp:90
static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR
Definition sipcall.cpp:81
static constexpr auto MULTIICE_REQUIRED_VERSION_STR
Definition sipcall.cpp:84
@ MEDIA_AUDIO
Definition media_codec.h:46
@ MEDIA_VIDEO
Definition media_codec.h:47
static void runOnMainThread(Callback &&cb)
Definition manager.h:930
static constexpr int ICE_COMP_COUNT_PER_STREAM
Definition sipcall.cpp:80
static constexpr const char SAMPLE_RATE[]
static constexpr char VIDEO_MAX_BITRATE[]
Definition call_const.h:63
static constexpr char PEER_HOLD[]
Definition call_const.h:52
static constexpr char AUDIO_CODEC[]
Definition call_const.h:57
static constexpr char VIDEO_MIN_BITRATE[]
Definition call_const.h:61
static constexpr char VIDEO_SOURCE[]
Definition call_const.h:55
static constexpr char VIDEO_CODEC[]
Definition call_const.h:59
static constexpr char AUDIO_SAMPLE_RATE[]
Definition call_const.h:58
static constexpr char VIDEO_BITRATE[]
Definition call_const.h:62
static constexpr char SOCKETS[]
Definition call_const.h:60
static constexpr char REGISTERED_NAME[]
Definition call_const.h:45
static constexpr const char * SEPARATOR
Definition media_const.h:32
static constexpr const char * FILE
Definition media_const.h:30
static constexpr char TLS_PEER_CA_[]
static constexpr char TLS_CIPHER[]
static constexpr char TLS_PEER_CERT[]
static constexpr char TLS_PEER_CA_NUM[]
bool hold(const std::string &accountId, const std::string &callId)
bool toggleRecording(const std::string &accountId, const std::string &callId)
bool resume(const std::string &accountId, const std::string &callId)
A SIP Account specify SIP specific functions and object = SIPCall/SIPVoIPLink)
SIPCall are SIP implementation of a normal Call.
Contains information about an AV subject.
Definition streamdata.h:30
const bool direction
Definition streamdata.h:52
const std::string id
Definition streamdata.h:50
const StreamType type
Definition streamdata.h:54
std::shared_ptr< MediaAttribute > remoteMediaAttribute_
Definition sipcall.h:86
std::shared_ptr< MediaAttribute > mediaAttribute_
Definition sipcall.h:85
#define jami_tracepoint(...)
Definition tracepoint.h:48