Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : *
4 : * Author: Emmanuel Milou <emmanuel.milou@savoirfairelinux.com>
5 : * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
6 : * Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>
7 : * Author: Eloi Bail <eloi.bail@savoirfairelinux.com>
8 : *
9 : * This program is free software; you can redistribute it and/or modify
10 : * it under the terms of the GNU General Public License as published by
11 : * the Free Software Foundation; either version 3 of the License, or
12 : * (at your option) any later version.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program; if not, write to the Free Software
21 : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 : */
23 :
24 : #include "sdp.h"
25 :
26 : #ifdef HAVE_CONFIG_H
27 : #include "config.h"
28 : #endif
29 :
30 : #include "sip/sipaccount.h"
31 : #include "sip/sipvoiplink.h"
32 : #include "string_utils.h"
33 : #include "base64.h"
34 :
35 : #include "manager.h"
36 : #include "logger.h"
37 : #include "libav_utils.h"
38 :
39 : #include "media_codec.h"
40 : #include "system_codec_container.h"
41 : #include "compiler_intrinsics.h" // for UNUSED
42 :
43 : #include <opendht/rng.h>
44 :
45 : #include <algorithm>
46 : #include <cassert>
47 :
48 : namespace jami {
49 :
50 : using std::string;
51 : using std::vector;
52 :
53 : static constexpr int POOL_INITIAL_SIZE = 16384;
54 : static constexpr int POOL_INCREMENT_SIZE = POOL_INITIAL_SIZE;
55 :
56 : static std::map<MediaDirection, const char*> DIRECTION_STR {{MediaDirection::SENDRECV, "sendrecv"},
57 : {MediaDirection::SENDONLY, "sendonly"},
58 : {MediaDirection::RECVONLY, "recvonly"},
59 : {MediaDirection::INACTIVE, "inactive"},
60 : {MediaDirection::UNKNOWN, "unknown"}};
61 :
62 406 : Sdp::Sdp(const std::string& id)
63 812 : : memPool_(nullptr, [](pj_pool_t* pool) { pj_pool_release(pool); })
64 406 : , publishedIpAddr_()
65 406 : , publishedIpAddrType_()
66 406 : , telephoneEventPayload_(101) // same as asterisk
67 812 : , sessionName_("Call ID " + id)
68 : {
69 406 : memPool_.reset(pj_pool_create(&Manager::instance().sipVoIPLink().getCachingPool()->factory,
70 : id.c_str(),
71 : POOL_INITIAL_SIZE,
72 : POOL_INCREMENT_SIZE,
73 : NULL));
74 406 : if (not memPool_)
75 0 : throw std::runtime_error("pj_pool_create() failed");
76 406 : }
77 :
78 406 : Sdp::~Sdp()
79 : {
80 406 : SIPAccount::releasePort(localAudioRtpPort_);
81 : #ifdef ENABLE_VIDEO
82 406 : SIPAccount::releasePort(localVideoRtpPort_);
83 : #endif
84 406 : }
85 :
86 : std::shared_ptr<SystemCodecInfo>
87 766 : Sdp::findCodecBySpec(std::string_view codec, const unsigned clockrate) const
88 : {
89 : // TODO : only manage a list?
90 1260 : for (const auto& accountCodec : audio_codec_list_) {
91 920 : auto audioCodecInfo = std::static_pointer_cast<SystemAudioCodecInfo>(accountCodec);
92 920 : if (audioCodecInfo->name == codec
93 1346 : and (audioCodecInfo->isPCMG722() ? (clockrate == 8000)
94 426 : : (audioCodecInfo->audioformat.sample_rate == clockrate)))
95 426 : return accountCodec;
96 920 : }
97 :
98 340 : for (const auto& accountCodec : video_codec_list_) {
99 340 : if (accountCodec->name == codec)
100 340 : return accountCodec;
101 : }
102 0 : return nullptr;
103 : }
104 :
105 : std::shared_ptr<SystemCodecInfo>
106 0 : Sdp::findCodecByPayload(const unsigned payloadType)
107 : {
108 : // TODO : only manage a list?
109 0 : for (const auto& accountCodec : audio_codec_list_) {
110 0 : if (accountCodec->payloadType == payloadType)
111 0 : return accountCodec;
112 : }
113 :
114 0 : for (const auto& accountCodec : video_codec_list_) {
115 0 : if (accountCodec->payloadType == payloadType)
116 0 : return accountCodec;
117 : }
118 0 : return nullptr;
119 : }
120 :
121 : static void
122 475 : randomFill(std::vector<uint8_t>& dest)
123 : {
124 475 : std::uniform_int_distribution<int> rand_byte {0, std::numeric_limits<uint8_t>::max()};
125 475 : std::random_device rdev;
126 475 : std::generate(dest.begin(), dest.end(), std::bind(rand_byte, std::ref(rdev)));
127 475 : }
128 :
129 : void
130 739 : Sdp::setActiveLocalSdpSession(const pjmedia_sdp_session* sdp)
131 : {
132 739 : if (activeLocalSession_ != sdp)
133 531 : JAMI_DBG("Set active local session to [%p]. Was [%p]", sdp, activeLocalSession_);
134 739 : activeLocalSession_ = sdp;
135 739 : }
136 :
137 : void
138 739 : Sdp::setActiveRemoteSdpSession(const pjmedia_sdp_session* sdp)
139 : {
140 739 : if (activeLocalSession_ != sdp)
141 339 : JAMI_DBG("Set active remote session to [%p]. Was [%p]", sdp, activeRemoteSession_);
142 739 : activeRemoteSession_ = sdp;
143 739 : }
144 :
145 : pjmedia_sdp_attr*
146 475 : Sdp::generateSdesAttribute()
147 : {
148 : static constexpr const unsigned cryptoSuite = 0;
149 475 : std::vector<uint8_t> keyAndSalt;
150 475 : keyAndSalt.resize(jami::CryptoSuites[cryptoSuite].masterKeyLength / 8
151 475 : + jami::CryptoSuites[cryptoSuite].masterSaltLength / 8);
152 : // generate keys
153 475 : randomFill(keyAndSalt);
154 :
155 950 : std::string crypto_attr = "1 "s + jami::CryptoSuites[cryptoSuite].name
156 1425 : + " inline:" + base64::encode(keyAndSalt);
157 475 : pj_str_t val {sip_utils::CONST_PJ_STR(crypto_attr)};
158 950 : return pjmedia_sdp_attr_create(memPool_.get(), "crypto", &val);
159 475 : }
160 :
161 : char const*
162 476 : Sdp::mediaDirection(const MediaAttribute& mediaAttr)
163 : {
164 476 : if (not mediaAttr.enabled_) {
165 0 : return DIRECTION_STR[MediaDirection::INACTIVE];
166 : }
167 :
168 : // Since mute/un-mute audio is only done locally (RTP packets
169 : // are still sent to the peer), the media direction must be
170 : // set to "sendrecv" regardless of the mute state.
171 476 : if (mediaAttr.type_ == MediaType::MEDIA_AUDIO) {
172 265 : return DIRECTION_STR[MediaDirection::SENDRECV];
173 : }
174 :
175 211 : if (mediaAttr.muted_) {
176 11 : if (mediaAttr.onHold_) {
177 0 : return DIRECTION_STR[MediaDirection::INACTIVE];
178 : }
179 11 : return DIRECTION_STR[MediaDirection::RECVONLY];
180 : }
181 :
182 200 : if (mediaAttr.onHold_) {
183 5 : return DIRECTION_STR[MediaDirection::SENDONLY];
184 : }
185 :
186 195 : return DIRECTION_STR[MediaDirection::SENDRECV];
187 : }
188 :
189 : MediaDirection
190 1393 : Sdp::getMediaDirection(pjmedia_sdp_media* media)
191 : {
192 2786 : if (pjmedia_sdp_attr_find2(media->attr_count,
193 1393 : media->attr,
194 1393 : DIRECTION_STR[MediaDirection::SENDRECV],
195 : nullptr)
196 1393 : != nullptr) {
197 1325 : return MediaDirection::SENDRECV;
198 : }
199 :
200 136 : if (pjmedia_sdp_attr_find2(media->attr_count,
201 68 : media->attr,
202 68 : DIRECTION_STR[MediaDirection::SENDONLY],
203 : nullptr)
204 68 : != nullptr) {
205 27 : return MediaDirection::SENDONLY;
206 : }
207 :
208 82 : if (pjmedia_sdp_attr_find2(media->attr_count,
209 41 : media->attr,
210 41 : DIRECTION_STR[MediaDirection::RECVONLY],
211 : nullptr)
212 41 : != nullptr) {
213 35 : return MediaDirection::RECVONLY;
214 : }
215 :
216 12 : if (pjmedia_sdp_attr_find2(media->attr_count,
217 6 : media->attr,
218 6 : DIRECTION_STR[MediaDirection::INACTIVE],
219 : nullptr)
220 6 : != nullptr) {
221 4 : return MediaDirection::INACTIVE;
222 : }
223 :
224 2 : return MediaDirection::UNKNOWN;
225 : }
226 :
227 : MediaTransport
228 627 : Sdp::getMediaTransport(pjmedia_sdp_media* media)
229 : {
230 627 : if (pj_stricmp2(&media->desc.transport, "RTP/SAVP") == 0)
231 626 : return MediaTransport::RTP_SAVP;
232 1 : else if (pj_stricmp2(&media->desc.transport, "RTP/AVP") == 0)
233 1 : return MediaTransport::RTP_AVP;
234 :
235 0 : return MediaTransport::UNKNOWN;
236 : }
237 :
238 : std::vector<std::string>
239 626 : Sdp::getCrypto(pjmedia_sdp_media* media)
240 : {
241 626 : std::vector<std::string> crypto;
242 9461 : for (unsigned j = 0; j < media->attr_count; j++) {
243 8835 : const auto attribute = media->attr[j];
244 8835 : if (pj_stricmp2(&attribute->name, "crypto") == 0)
245 624 : crypto.emplace_back(attribute->value.ptr, attribute->value.slen);
246 : }
247 :
248 626 : return crypto;
249 0 : }
250 :
251 : pjmedia_sdp_media*
252 476 : Sdp::addMediaDescription(const MediaAttribute& mediaAttr)
253 : {
254 476 : auto type = mediaAttr.type_;
255 476 : auto secure = mediaAttr.secure_;
256 :
257 476 : JAMI_DBG("Add media description [%s]", mediaAttr.toString(true).c_str());
258 :
259 476 : pjmedia_sdp_media* med = PJ_POOL_ZALLOC_T(memPool_.get(), pjmedia_sdp_media);
260 :
261 476 : switch (type) {
262 265 : case MediaType::MEDIA_AUDIO:
263 265 : med->desc.media = sip_utils::CONST_PJ_STR("audio");
264 265 : med->desc.port = mediaAttr.enabled_ ? localAudioRtpPort_ : 0;
265 265 : med->desc.fmt_count = audio_codec_list_.size();
266 265 : break;
267 211 : case MediaType::MEDIA_VIDEO:
268 211 : med->desc.media = sip_utils::CONST_PJ_STR("video");
269 211 : med->desc.port = mediaAttr.enabled_ ? localVideoRtpPort_ : 0;
270 211 : med->desc.fmt_count = video_codec_list_.size();
271 211 : break;
272 0 : default:
273 0 : throw SdpException("Unsupported media type! Only audio and video are supported");
274 : break;
275 : }
276 :
277 476 : med->desc.port_count = 1;
278 :
279 : // Set the transport protocol of the media
280 476 : med->desc.transport = secure ? sip_utils::CONST_PJ_STR("RTP/SAVP")
281 1 : : sip_utils::CONST_PJ_STR("RTP/AVP");
282 :
283 476 : unsigned dynamic_payload = 96;
284 :
285 1360 : for (unsigned i = 0; i < med->desc.fmt_count; i++) {
286 : pjmedia_sdp_rtpmap rtpmap;
287 884 : rtpmap.param.slen = 0;
288 :
289 884 : std::string channels; // must have the lifetime of rtpmap
290 884 : std::string enc_name;
291 : unsigned payload;
292 :
293 884 : if (type == MediaType::MEDIA_AUDIO) {
294 : auto accountAudioCodec = std::static_pointer_cast<SystemAudioCodecInfo>(
295 440 : audio_codec_list_[i]);
296 440 : payload = accountAudioCodec->payloadType;
297 440 : enc_name = accountAudioCodec->name;
298 :
299 440 : if (accountAudioCodec->audioformat.nb_channels > 1) {
300 265 : channels = std::to_string(accountAudioCodec->audioformat.nb_channels);
301 265 : rtpmap.param = sip_utils::CONST_PJ_STR(channels);
302 : }
303 : // G722 requires G722/8000 media description even though it's @ 16000 Hz
304 : // See http://tools.ietf.org/html/rfc3551#section-4.5.2
305 440 : if (accountAudioCodec->isPCMG722())
306 25 : rtpmap.clock_rate = 8000;
307 : else
308 415 : rtpmap.clock_rate = accountAudioCodec->audioformat.sample_rate;
309 :
310 440 : } else {
311 : // FIXME: get this key from header
312 444 : payload = dynamic_payload++;
313 444 : enc_name = video_codec_list_[i]->name;
314 444 : rtpmap.clock_rate = 90000;
315 : }
316 :
317 884 : auto payloadStr = std::to_string(payload);
318 884 : auto pjPayload = sip_utils::CONST_PJ_STR(payloadStr);
319 884 : pj_strdup(memPool_.get(), &med->desc.fmt[i], &pjPayload);
320 :
321 : // Add a rtpmap field for each codec
322 : // We could add one only for dynamic payloads because the codecs with static RTP payloads
323 : // are entirely defined in the RFC 3351
324 884 : rtpmap.pt = med->desc.fmt[i];
325 884 : rtpmap.enc_name = sip_utils::CONST_PJ_STR(enc_name);
326 :
327 : pjmedia_sdp_attr* attr;
328 884 : pjmedia_sdp_rtpmap_to_attr(memPool_.get(), &rtpmap, &attr);
329 884 : med->attr[med->attr_count++] = attr;
330 :
331 : #ifdef ENABLE_VIDEO
332 884 : if (enc_name == "H264") {
333 : // FIXME: this should not be hardcoded, it will determine what profile and level
334 : // our peer will send us
335 : const auto accountVideoCodec = std::static_pointer_cast<SystemVideoCodecInfo>(
336 211 : video_codec_list_[i]);
337 211 : const auto& profileLevelID = accountVideoCodec->parameters.empty()
338 : ? libav_utils::DEFAULT_H264_PROFILE_LEVEL_ID
339 211 : : accountVideoCodec->parameters;
340 0 : auto value = fmt::format("fmtp:{} {}", payload, profileLevelID);
341 211 : med->attr[med->attr_count++] = pjmedia_sdp_attr_create(memPool_.get(),
342 : value.c_str(),
343 : NULL);
344 211 : }
345 : #endif
346 884 : }
347 :
348 476 : if (type == MediaType::MEDIA_AUDIO) {
349 265 : setTelephoneEventRtpmap(med);
350 265 : if (localAudioRtcpPort_) {
351 265 : addRTCPAttribute(med, localAudioRtcpPort_);
352 : }
353 211 : } else if (type == MediaType::MEDIA_VIDEO and localVideoRtcpPort_) {
354 211 : addRTCPAttribute(med, localVideoRtcpPort_);
355 : }
356 :
357 476 : char const* direction = mediaDirection(mediaAttr);
358 :
359 476 : med->attr[med->attr_count++] = pjmedia_sdp_attr_create(memPool_.get(), direction, NULL);
360 :
361 476 : if (secure) {
362 475 : if (pjmedia_sdp_media_add_attr(med, generateSdesAttribute()) != PJ_SUCCESS)
363 0 : throw SdpException("Could not add sdes attribute to media");
364 : }
365 :
366 476 : return med;
367 : }
368 :
369 : void
370 476 : Sdp::addRTCPAttribute(pjmedia_sdp_media* med, uint16_t port)
371 : {
372 476 : dhtnet::IpAddr addr {publishedIpAddr_};
373 476 : addr.setPort(port);
374 476 : pjmedia_sdp_attr* attr = pjmedia_sdp_attr_create_rtcp(memPool_.get(), addr.pjPtr());
375 476 : if (attr)
376 476 : pjmedia_sdp_attr_add(&med->attr_count, med->attr, attr);
377 476 : }
378 :
379 : void
380 214 : Sdp::setPublishedIP(const std::string& addr, pj_uint16_t addr_type)
381 : {
382 214 : publishedIpAddr_ = addr;
383 214 : publishedIpAddrType_ = addr_type;
384 214 : if (localSession_) {
385 0 : if (addr_type == pj_AF_INET6())
386 0 : localSession_->origin.addr_type = sip_utils::CONST_PJ_STR("IP6");
387 : else
388 0 : localSession_->origin.addr_type = sip_utils::CONST_PJ_STR("IP4");
389 0 : localSession_->origin.addr = sip_utils::CONST_PJ_STR(publishedIpAddr_);
390 0 : localSession_->conn->addr = localSession_->origin.addr;
391 0 : if (pjmedia_sdp_validate(localSession_) != PJ_SUCCESS)
392 0 : JAMI_ERR("Could not validate SDP");
393 : }
394 214 : }
395 :
396 : void
397 214 : Sdp::setPublishedIP(const dhtnet::IpAddr& ip_addr)
398 : {
399 214 : setPublishedIP(ip_addr, ip_addr.getFamily());
400 214 : }
401 :
402 : void
403 265 : Sdp::setTelephoneEventRtpmap(pjmedia_sdp_media* med)
404 : {
405 265 : ++med->desc.fmt_count;
406 530 : pj_strdup2(memPool_.get(),
407 265 : &med->desc.fmt[med->desc.fmt_count - 1],
408 530 : std::to_string(telephoneEventPayload_).c_str());
409 :
410 : pjmedia_sdp_attr* attr_rtpmap = static_cast<pjmedia_sdp_attr*>(
411 265 : pj_pool_zalloc(memPool_.get(), sizeof(pjmedia_sdp_attr)));
412 265 : attr_rtpmap->name = sip_utils::CONST_PJ_STR("rtpmap");
413 265 : attr_rtpmap->value = sip_utils::CONST_PJ_STR("101 telephone-event/8000");
414 :
415 265 : med->attr[med->attr_count++] = attr_rtpmap;
416 :
417 : pjmedia_sdp_attr* attr_fmtp = static_cast<pjmedia_sdp_attr*>(
418 265 : pj_pool_zalloc(memPool_.get(), sizeof(pjmedia_sdp_attr)));
419 265 : attr_fmtp->name = sip_utils::CONST_PJ_STR("fmtp");
420 265 : attr_fmtp->value = sip_utils::CONST_PJ_STR("101 0-15");
421 :
422 265 : med->attr[med->attr_count++] = attr_fmtp;
423 265 : }
424 :
425 : void
426 812 : Sdp::setLocalMediaCapabilities(MediaType type,
427 : const std::vector<std::shared_ptr<SystemCodecInfo>>& selectedCodecs)
428 : {
429 812 : switch (type) {
430 406 : case MediaType::MEDIA_AUDIO:
431 406 : audio_codec_list_ = selectedCodecs;
432 406 : break;
433 :
434 406 : case MediaType::MEDIA_VIDEO:
435 : #ifdef ENABLE_VIDEO
436 406 : video_codec_list_ = selectedCodecs;
437 : // Do not expose H265 if accel is disactivated
438 406 : if (not jami::Manager::instance().videoPreferences.getEncodingAccelerated()) {
439 406 : video_codec_list_.erase(std::remove_if(video_codec_list_.begin(),
440 : video_codec_list_.end(),
441 852 : [](const std::shared_ptr<SystemCodecInfo>& i) {
442 852 : return i->name == "H265";
443 : }),
444 812 : video_codec_list_.end());
445 : }
446 : #else
447 : (void) selectedCodecs;
448 : #endif
449 406 : break;
450 :
451 0 : default:
452 0 : throw SdpException("Unsupported media type");
453 : break;
454 : }
455 812 : }
456 :
457 : const char*
458 978 : Sdp::getSdpDirectionStr(SdpDirection direction)
459 : {
460 978 : if (direction == SdpDirection::OFFER)
461 542 : return "OFFER";
462 436 : if (direction == SdpDirection::ANSWER)
463 436 : return "ANSWER";
464 0 : return "NONE";
465 : }
466 :
467 : void
468 978 : Sdp::printSession(const pjmedia_sdp_session* session, const char* header, SdpDirection direction)
469 : {
470 : static constexpr size_t BUF_SZ = 4095;
471 : std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&>
472 978 : tmpPool_(pj_pool_create(&Manager::instance().sipVoIPLink().getCachingPool()->factory,
473 : "printSdp",
474 : BUF_SZ,
475 : BUF_SZ,
476 : nullptr),
477 978 : pj_pool_release);
478 :
479 978 : auto cloned_session = pjmedia_sdp_session_clone(tmpPool_.get(), session);
480 978 : if (!cloned_session) {
481 0 : JAMI_ERR("Could not clone SDP for printing");
482 0 : return;
483 : }
484 :
485 : // Filter-out sensible data like SRTP master key.
486 2781 : for (unsigned i = 0; i < cloned_session->media_count; ++i) {
487 1803 : pjmedia_sdp_media_remove_all_attr(cloned_session->media[i], "crypto");
488 : }
489 :
490 : std::array<char, BUF_SZ + 1> buffer;
491 978 : auto size = pjmedia_sdp_print(cloned_session, buffer.data(), BUF_SZ);
492 978 : if (size < 0) {
493 0 : JAMI_ERR("%s SDP too big for dump", header);
494 0 : return;
495 : }
496 :
497 978 : JAMI_DBG("[SDP %s] %s\n%.*s", getSdpDirectionStr(direction), header, size, buffer.data());
498 978 : }
499 :
500 : void
501 263 : Sdp::createLocalSession(SdpDirection direction)
502 : {
503 263 : sdpDirection_ = direction;
504 263 : localSession_ = PJ_POOL_ZALLOC_T(memPool_.get(), pjmedia_sdp_session);
505 263 : localSession_->conn = PJ_POOL_ZALLOC_T(memPool_.get(), pjmedia_sdp_conn);
506 :
507 : /* Initialize the fields of the struct */
508 263 : localSession_->origin.version = 0;
509 : pj_time_val tv;
510 263 : pj_gettimeofday(&tv);
511 :
512 263 : localSession_->origin.user = *pj_gethostname();
513 :
514 : // Use Network Time Protocol format timestamp to ensure uniqueness.
515 263 : localSession_->origin.id = tv.sec + 2208988800UL;
516 263 : localSession_->origin.net_type = sip_utils::CONST_PJ_STR("IN");
517 263 : if (publishedIpAddrType_ == pj_AF_INET6())
518 0 : localSession_->origin.addr_type = sip_utils::CONST_PJ_STR("IP6");
519 : else
520 263 : localSession_->origin.addr_type = sip_utils::CONST_PJ_STR("IP4");
521 263 : localSession_->origin.addr = sip_utils::CONST_PJ_STR(publishedIpAddr_);
522 :
523 : // Use the call IDs for s= line
524 263 : localSession_->name = sip_utils::CONST_PJ_STR(sessionName_);
525 :
526 263 : localSession_->conn->net_type = localSession_->origin.net_type;
527 263 : localSession_->conn->addr_type = localSession_->origin.addr_type;
528 263 : localSession_->conn->addr = localSession_->origin.addr;
529 :
530 : // RFC 3264: An offer/answer model session description protocol
531 : // As the session is created and destroyed through an external signaling mean (SIP), the line
532 : // should have a value of "0 0".
533 263 : localSession_->time.start = 0;
534 263 : localSession_->time.stop = 0;
535 263 : }
536 :
537 : int
538 526 : Sdp::validateSession() const
539 : {
540 526 : return pjmedia_sdp_validate(localSession_);
541 : }
542 :
543 : bool
544 137 : Sdp::createOffer(const std::vector<MediaAttribute>& mediaList)
545 : {
546 137 : if (mediaList.size() >= PJMEDIA_MAX_SDP_MEDIA) {
547 0 : throw SdpException("Media list size exceeds SDP media maximum size");
548 : }
549 411 : JAMI_DEBUG("Creating SDP offer with {} media", mediaList.size());
550 :
551 137 : createLocalSession(SdpDirection::OFFER);
552 :
553 137 : if (validateSession() != PJ_SUCCESS) {
554 0 : JAMI_ERR("Failed to create initial offer");
555 0 : return false;
556 : }
557 :
558 137 : localSession_->media_count = 0;
559 :
560 386 : for (auto const& media : mediaList) {
561 249 : if (media.enabled_) {
562 249 : localSession_->media[localSession_->media_count++] = addMediaDescription(media);
563 : }
564 : }
565 :
566 137 : if (validateSession() != PJ_SUCCESS) {
567 0 : JAMI_ERR("Failed to add medias");
568 0 : return false;
569 : }
570 :
571 137 : if (pjmedia_sdp_neg_create_w_local_offer(memPool_.get(), localSession_, &negotiator_)
572 137 : != PJ_SUCCESS) {
573 0 : JAMI_ERR("Failed to create an initial SDP negotiator");
574 0 : return false;
575 : }
576 :
577 137 : printSession(localSession_, "Local session (initial):", sdpDirection_);
578 :
579 137 : return true;
580 : }
581 :
582 : void
583 136 : Sdp::setReceivedOffer(const pjmedia_sdp_session* remote)
584 : {
585 136 : if (remote == nullptr) {
586 0 : JAMI_ERR("Remote session is NULL");
587 0 : return;
588 : }
589 136 : remoteSession_ = pjmedia_sdp_session_clone(memPool_.get(), remote);
590 : }
591 :
592 : bool
593 126 : Sdp::processIncomingOffer(const std::vector<MediaAttribute>& mediaList)
594 : {
595 126 : if (not remoteSession_)
596 0 : return false;
597 :
598 378 : JAMI_DEBUG("Processing received offer for [{:s}] with {:d} media",
599 : sessionName_,
600 : mediaList.size());
601 :
602 126 : printSession(remoteSession_, "Remote session:", SdpDirection::OFFER);
603 :
604 126 : createLocalSession(SdpDirection::ANSWER);
605 126 : if (validateSession() != PJ_SUCCESS) {
606 0 : JAMI_ERR("Failed to create local session");
607 0 : return false;
608 : }
609 :
610 126 : localSession_->media_count = 0;
611 :
612 356 : for (auto const& media : mediaList) {
613 230 : if (media.enabled_) {
614 227 : localSession_->media[localSession_->media_count++] = addMediaDescription(media);
615 : }
616 : }
617 :
618 126 : printSession(localSession_, "Local session:\n", sdpDirection_);
619 :
620 126 : if (validateSession() != PJ_SUCCESS) {
621 0 : JAMI_ERR("Failed to add medias");
622 0 : return false;
623 : }
624 :
625 126 : if (pjmedia_sdp_neg_create_w_remote_offer(memPool_.get(),
626 126 : localSession_,
627 126 : remoteSession_,
628 : &negotiator_)
629 126 : != PJ_SUCCESS) {
630 0 : JAMI_ERR("Failed to initialize media negotiation");
631 0 : return false;
632 : }
633 :
634 126 : return true;
635 : }
636 :
637 : bool
638 29 : Sdp::startNegotiation()
639 : {
640 29 : JAMI_DBG("Starting media negotiation for [%s]", sessionName_.c_str());
641 :
642 29 : if (negotiator_ == NULL) {
643 0 : JAMI_ERR("Can't start negotiation with invalid negotiator");
644 0 : return false;
645 : }
646 :
647 : const pjmedia_sdp_session* active_local;
648 : const pjmedia_sdp_session* active_remote;
649 :
650 29 : if (pjmedia_sdp_neg_get_state(negotiator_) != PJMEDIA_SDP_NEG_STATE_WAIT_NEGO) {
651 0 : JAMI_WARN("Negotiator not in right state for negotiation");
652 0 : return false;
653 : }
654 :
655 29 : if (pjmedia_sdp_neg_negotiate(memPool_.get(), negotiator_, 0) != PJ_SUCCESS) {
656 0 : JAMI_ERR("Failed to start media negotiation");
657 0 : return false;
658 : }
659 :
660 29 : if (pjmedia_sdp_neg_get_active_local(negotiator_, &active_local) != PJ_SUCCESS)
661 0 : JAMI_ERR("Could not retrieve local active session");
662 :
663 29 : setActiveLocalSdpSession(active_local);
664 :
665 29 : if (active_local != nullptr) {
666 29 : printSession(active_local, "Local active session:", sdpDirection_);
667 : }
668 :
669 29 : if (pjmedia_sdp_neg_get_active_remote(negotiator_, &active_remote) != PJ_SUCCESS
670 29 : or active_remote == nullptr) {
671 0 : JAMI_ERR("Could not retrieve remote active session");
672 0 : return false;
673 : }
674 :
675 29 : setActiveRemoteSdpSession(active_remote);
676 :
677 29 : printSession(active_remote, "Remote active session:", sdpDirection_);
678 :
679 29 : return true;
680 : }
681 :
682 : std::string
683 400 : Sdp::getFilteredSdp(const pjmedia_sdp_session* session, unsigned media_keep, unsigned pt_keep)
684 : {
685 : static constexpr size_t BUF_SZ = 4096;
686 : std::unique_ptr<pj_pool_t, decltype(pj_pool_release)&>
687 400 : tmpPool_(pj_pool_create(&Manager::instance().sipVoIPLink().getCachingPool()->factory,
688 : "tmpSdp",
689 : BUF_SZ,
690 : BUF_SZ,
691 : nullptr),
692 400 : pj_pool_release);
693 400 : auto cloned = pjmedia_sdp_session_clone(tmpPool_.get(), session);
694 400 : if (!cloned) {
695 0 : JAMI_ERR("Could not clone SDP");
696 0 : return "";
697 : }
698 :
699 : // deactivate non-video media
700 400 : bool hasKeep = false;
701 1176 : for (unsigned i = 0; i < cloned->media_count; i++)
702 776 : if (i != media_keep) {
703 376 : if (pjmedia_sdp_media_deactivate(tmpPool_.get(), cloned->media[i]) != PJ_SUCCESS)
704 0 : JAMI_ERR("Could not deactivate media");
705 : } else {
706 400 : hasKeep = true;
707 : }
708 :
709 400 : if (not hasKeep) {
710 0 : JAMI_DBG("No media to keep present in SDP");
711 0 : return "";
712 : }
713 :
714 : // Leaking medias will be dropped with tmpPool_
715 1176 : for (unsigned i = 0; i < cloned->media_count; i++)
716 776 : if (cloned->media[i]->desc.port == 0) {
717 376 : std::move(cloned->media + i + 1, cloned->media + cloned->media_count, cloned->media + i);
718 376 : cloned->media_count--;
719 376 : i--;
720 : }
721 :
722 800 : for (unsigned i = 0; i < cloned->media_count; i++) {
723 400 : auto media = cloned->media[i];
724 :
725 : // filter other codecs
726 1023 : for (unsigned c = 0; c < media->desc.fmt_count; c++) {
727 623 : auto& pt = media->desc.fmt[c];
728 623 : if (pj_strtoul(&pt) == pt_keep)
729 400 : continue;
730 :
731 446 : while (auto attr = pjmedia_sdp_attr_find2(media->attr_count, media->attr, "rtpmap", &pt))
732 223 : pjmedia_sdp_attr_remove(&media->attr_count, media->attr, attr);
733 :
734 223 : while (auto attr = pjmedia_sdp_attr_find2(media->attr_count, media->attr, "fmt", &pt))
735 0 : pjmedia_sdp_attr_remove(&media->attr_count, media->attr, attr);
736 :
737 223 : std::move(media->desc.fmt + c + 1,
738 223 : media->desc.fmt + media->desc.fmt_count,
739 223 : media->desc.fmt + c);
740 223 : media->desc.fmt_count--;
741 223 : c--;
742 : }
743 :
744 : // we handle crypto ourselfs, don't tell libav about it
745 400 : pjmedia_sdp_media_remove_all_attr(media, "crypto");
746 : }
747 :
748 : char buffer[BUF_SZ];
749 400 : size_t size = pjmedia_sdp_print(cloned, buffer, sizeof(buffer));
750 400 : string sessionStr(buffer, std::min(size, sizeof(buffer)));
751 :
752 400 : return sessionStr;
753 400 : }
754 :
755 : std::vector<MediaDescription>
756 34 : Sdp::getActiveMediaDescription(bool remote) const
757 : {
758 34 : if (remote)
759 7 : return getMediaDescriptions(activeRemoteSession_, true);
760 :
761 27 : return getMediaDescriptions(activeLocalSession_, false);
762 : }
763 :
764 : std::vector<MediaDescription>
765 420 : Sdp::getMediaDescriptions(const pjmedia_sdp_session* session, bool remote) const
766 : {
767 420 : if (!session)
768 2 : return {};
769 : static constexpr pj_str_t STR_RTPMAP {sip_utils::CONST_PJ_STR("rtpmap")};
770 : static constexpr pj_str_t STR_FMTP {sip_utils::CONST_PJ_STR("fmtp")};
771 :
772 418 : std::vector<MediaDescription> ret;
773 1190 : for (unsigned i = 0; i < session->media_count; i++) {
774 772 : auto media = session->media[i];
775 772 : ret.emplace_back(MediaDescription());
776 772 : MediaDescription& descr = ret.back();
777 772 : if (!pj_stricmp2(&media->desc.media, "audio"))
778 426 : descr.type = MEDIA_AUDIO;
779 346 : else if (!pj_stricmp2(&media->desc.media, "video"))
780 346 : descr.type = MEDIA_VIDEO;
781 : else
782 6 : continue;
783 :
784 772 : descr.enabled = media->desc.port;
785 772 : if (!descr.enabled)
786 6 : continue;
787 :
788 : // get connection info
789 766 : pjmedia_sdp_conn* conn = media->conn ? media->conn : session->conn;
790 766 : if (not conn) {
791 0 : JAMI_ERR("Could not find connection information for media");
792 0 : continue;
793 : }
794 766 : descr.addr = std::string_view(conn->addr.ptr, conn->addr.slen);
795 766 : descr.addr.setPort(media->desc.port);
796 :
797 : // Get the "rtcp" address from the SDP if present. Otherwise,
798 : // infere it from endpoint (RTP) address.
799 766 : auto attr = pjmedia_sdp_attr_find2(media->attr_count, media->attr, "rtcp", NULL);
800 766 : if (attr) {
801 : pjmedia_sdp_rtcp_attr rtcp;
802 766 : auto status = pjmedia_sdp_attr_get_rtcp(attr, &rtcp);
803 766 : if (status == PJ_SUCCESS && rtcp.addr.slen) {
804 766 : descr.rtcp_addr = std::string_view(rtcp.addr.ptr, rtcp.addr.slen);
805 766 : descr.rtcp_addr.setPort(rtcp.port);
806 : }
807 : }
808 :
809 2298 : descr.onHold = pjmedia_sdp_attr_find2(media->attr_count,
810 766 : media->attr,
811 766 : DIRECTION_STR[MediaDirection::SENDONLY],
812 : nullptr)
813 1519 : || pjmedia_sdp_attr_find2(media->attr_count,
814 753 : media->attr,
815 753 : DIRECTION_STR[MediaDirection::INACTIVE],
816 : nullptr);
817 :
818 766 : descr.direction_ = getMediaDirection(media);
819 766 : if (descr.direction_ == MediaDirection::UNKNOWN) {
820 0 : JAMI_ERR("Did not find media direction attribute in remote SDP");
821 : }
822 :
823 : // get codecs infos
824 766 : for (unsigned j = 0; j < media->desc.fmt_count; j++) {
825 1532 : const auto rtpMapAttribute = pjmedia_sdp_media_find_attr(media,
826 : &STR_RTPMAP,
827 766 : &media->desc.fmt[j]);
828 766 : if (!rtpMapAttribute) {
829 0 : JAMI_ERR("Could not find rtpmap attribute");
830 0 : descr.enabled = false;
831 0 : continue;
832 : }
833 : pjmedia_sdp_rtpmap rtpmap;
834 766 : if (pjmedia_sdp_attr_get_rtpmap(rtpMapAttribute, &rtpmap) != PJ_SUCCESS
835 766 : || rtpmap.enc_name.slen == 0) {
836 0 : JAMI_ERR("Could not find payload type %.*s in SDP",
837 : (int) media->desc.fmt[j].slen,
838 : media->desc.fmt[j].ptr);
839 0 : descr.enabled = false;
840 0 : continue;
841 : }
842 766 : auto codec_raw = sip_utils::as_view(rtpmap.enc_name);
843 766 : descr.rtp_clockrate = rtpmap.clock_rate;
844 766 : descr.codec = findCodecBySpec(codec_raw, rtpmap.clock_rate);
845 766 : if (not descr.codec) {
846 0 : JAMI_ERR("Could not find codec %.*s", (int) codec_raw.size(), codec_raw.data());
847 0 : descr.enabled = false;
848 0 : continue;
849 : }
850 766 : descr.payload_type = pj_strtoul(&rtpmap.pt);
851 766 : if (descr.type == MEDIA_VIDEO) {
852 680 : const auto fmtpAttr = pjmedia_sdp_media_find_attr(media,
853 : &STR_FMTP,
854 340 : &media->desc.fmt[j]);
855 : // descr.bitrate = getOutgoingVideoField(codec, "bitrate");
856 340 : if (fmtpAttr && fmtpAttr->value.ptr && fmtpAttr->value.slen) {
857 163 : const auto& v = fmtpAttr->value;
858 163 : descr.parameters = std::string(v.ptr, v.ptr + v.slen);
859 : }
860 : }
861 : // for now, just keep the first codec only
862 766 : descr.enabled = true;
863 766 : break;
864 : }
865 :
866 766 : if (not remote)
867 400 : descr.receiving_sdp = getFilteredSdp(session, i, descr.payload_type);
868 :
869 : // get crypto info
870 766 : std::vector<std::string> crypto;
871 11933 : for (unsigned j = 0; j < media->attr_count; j++) {
872 11167 : const auto attribute = media->attr[j];
873 11167 : if (pj_stricmp2(&attribute->name, "crypto") == 0)
874 766 : crypto.emplace_back(attribute->value.ptr, attribute->value.slen);
875 : }
876 766 : descr.crypto = SdesNegotiator::negotiate(crypto);
877 766 : }
878 418 : return ret;
879 418 : }
880 :
881 : std::vector<Sdp::MediaSlot>
882 193 : Sdp::getMediaSlots() const
883 : {
884 193 : auto loc = getMediaDescriptions(activeLocalSession_, false);
885 193 : auto rem = getMediaDescriptions(activeRemoteSession_, true);
886 193 : size_t slot_n = std::min(loc.size(), rem.size());
887 193 : std::vector<MediaSlot> s;
888 193 : s.reserve(slot_n);
889 546 : for (decltype(slot_n) i = 0; i < slot_n; i++)
890 353 : s.emplace_back(std::move(loc[i]), std::move(rem[i]));
891 386 : return s;
892 193 : }
893 :
894 : void
895 828 : Sdp::addIceCandidates(unsigned media_index, const std::vector<std::string>& cands)
896 : {
897 828 : if (media_index >= localSession_->media_count) {
898 0 : JAMI_ERR("addIceCandidates failed: cannot access media#%u (may be deactivated)",
899 : media_index);
900 0 : return;
901 : }
902 :
903 828 : auto media = localSession_->media[media_index];
904 :
905 4724 : for (const auto& item : cands) {
906 3896 : const pj_str_t val = sip_utils::CONST_PJ_STR(item);
907 3896 : pjmedia_sdp_attr* attr = pjmedia_sdp_attr_create(memPool_.get(), "candidate", &val);
908 :
909 3896 : if (pjmedia_sdp_media_add_attr(media, attr) != PJ_SUCCESS)
910 0 : throw SdpException("Could not add ICE candidates attribute to media");
911 : }
912 : }
913 :
914 : std::vector<std::string>
915 393 : Sdp::getIceCandidates(unsigned media_index) const
916 : {
917 393 : auto remoteSession = activeRemoteSession_ ? activeRemoteSession_ : remoteSession_;
918 393 : auto localSession = activeLocalSession_ ? activeLocalSession_ : localSession_;
919 393 : if (not remoteSession) {
920 0 : JAMI_ERR("getIceCandidates failed: no remote session");
921 0 : return {};
922 : }
923 393 : if (not localSession) {
924 0 : JAMI_ERR("getIceCandidates failed: no local session");
925 0 : return {};
926 : }
927 393 : if (media_index >= remoteSession->media_count || media_index >= localSession->media_count) {
928 0 : JAMI_ERR("getIceCandidates failed: cannot access media#%u (may be deactivated)",
929 : media_index);
930 0 : return {};
931 : }
932 393 : auto media = remoteSession->media[media_index];
933 393 : auto localMedia = localSession->media[media_index];
934 393 : if (media->desc.port == 0 || localMedia->desc.port == 0) {
935 5 : JAMI_WARN("Media#%u is disabled. Media ports: local %u, remote %u",
936 : media_index,
937 : localMedia->desc.port,
938 : media->desc.port);
939 5 : return {};
940 : }
941 :
942 388 : std::vector<std::string> candidates;
943 :
944 6380 : for (unsigned i = 0; i < media->attr_count; i++) {
945 5992 : pjmedia_sdp_attr* attribute = media->attr[i];
946 5992 : if (pj_stricmp2(&attribute->name, "candidate") == 0)
947 3680 : candidates.push_back(std::string(attribute->value.ptr, attribute->value.slen));
948 : }
949 :
950 388 : return candidates;
951 388 : }
952 :
953 : void
954 229 : Sdp::addIceAttributes(const dhtnet::IceTransport::Attribute&& ice_attrs)
955 : {
956 229 : pj_str_t value = sip_utils::CONST_PJ_STR(ice_attrs.ufrag);
957 229 : pjmedia_sdp_attr* attr = pjmedia_sdp_attr_create(memPool_.get(), "ice-ufrag", &value);
958 :
959 229 : if (pjmedia_sdp_attr_add(&localSession_->attr_count, localSession_->attr, attr) != PJ_SUCCESS)
960 0 : throw SdpException("Could not add ICE.ufrag attribute to local SDP");
961 :
962 229 : value = sip_utils::CONST_PJ_STR(ice_attrs.pwd);
963 229 : attr = pjmedia_sdp_attr_create(memPool_.get(), "ice-pwd", &value);
964 :
965 229 : if (pjmedia_sdp_attr_add(&localSession_->attr_count, localSession_->attr, attr) != PJ_SUCCESS)
966 0 : throw SdpException("Could not add ICE.pwd attribute to local SDP");
967 229 : }
968 :
969 : dhtnet::IceTransport::Attribute
970 581 : Sdp::getIceAttributes() const
971 : {
972 581 : if (auto session = activeRemoteSession_ ? activeRemoteSession_ : remoteSession_)
973 580 : return getIceAttributes(session);
974 1 : return {};
975 : }
976 :
977 : dhtnet::IceTransport::Attribute
978 580 : Sdp::getIceAttributes(const pjmedia_sdp_session* session)
979 : {
980 580 : dhtnet::IceTransport::Attribute ice_attrs;
981 : // Per RFC8839, ice-ufrag/ice-pwd can be present either at
982 : // media or session level.
983 : // This seems to be the case for Asterisk servers (ICE is at media-session).
984 1119 : for (unsigned i = 0; i < session->attr_count; i++) {
985 1078 : pjmedia_sdp_attr* attribute = session->attr[i];
986 1078 : if (pj_stricmp2(&attribute->name, "ice-ufrag") == 0)
987 539 : ice_attrs.ufrag.assign(attribute->value.ptr, attribute->value.slen);
988 539 : else if (pj_stricmp2(&attribute->name, "ice-pwd") == 0)
989 539 : ice_attrs.pwd.assign(attribute->value.ptr, attribute->value.slen);
990 1078 : if (!ice_attrs.ufrag.empty() && !ice_attrs.pwd.empty())
991 539 : return ice_attrs;
992 : }
993 120 : for (unsigned i = 0; i < session->media_count; i++) {
994 79 : auto* media = session->media[i];
995 560 : for (unsigned j = 0; j < media->attr_count; j++) {
996 481 : pjmedia_sdp_attr* attribute = media->attr[j];
997 481 : if (pj_stricmp2(&attribute->name, "ice-ufrag") == 0)
998 0 : ice_attrs.ufrag.assign(attribute->value.ptr, attribute->value.slen);
999 481 : else if (pj_stricmp2(&attribute->name, "ice-pwd") == 0)
1000 0 : ice_attrs.pwd.assign(attribute->value.ptr, attribute->value.slen);
1001 481 : if (!ice_attrs.ufrag.empty() && !ice_attrs.pwd.empty())
1002 0 : return ice_attrs;
1003 : }
1004 : }
1005 :
1006 41 : return ice_attrs;
1007 0 : }
1008 :
1009 : void
1010 59 : Sdp::clearIce()
1011 : {
1012 59 : clearIce(localSession_);
1013 59 : clearIce(remoteSession_);
1014 59 : setActiveRemoteSdpSession(nullptr);
1015 59 : setActiveLocalSdpSession(nullptr);
1016 59 : }
1017 :
1018 : void
1019 118 : Sdp::clearIce(pjmedia_sdp_session* session)
1020 : {
1021 118 : if (not session)
1022 29 : return;
1023 89 : pjmedia_sdp_attr_remove_all(&session->attr_count, session->attr, "ice-ufrag");
1024 89 : pjmedia_sdp_attr_remove_all(&session->attr_count, session->attr, "ice-pwd");
1025 : // TODO. Why this? we should not have "candidate" attribute at session level.
1026 89 : pjmedia_sdp_attr_remove_all(&session->attr_count, session->attr, "candidate");
1027 252 : for (unsigned i = 0; i < session->media_count; i++) {
1028 163 : auto media = session->media[i];
1029 163 : pjmedia_sdp_attr_remove_all(&media->attr_count, media->attr, "candidate");
1030 : }
1031 : }
1032 :
1033 : std::vector<MediaAttribute>
1034 343 : Sdp::getMediaAttributeListFromSdp(const pjmedia_sdp_session* sdpSession, bool ignoreDisabled)
1035 : {
1036 343 : if (sdpSession == nullptr) {
1037 2 : return {};
1038 : }
1039 :
1040 341 : std::vector<MediaAttribute> mediaList;
1041 341 : unsigned audioIdx = 0;
1042 341 : unsigned videoIdx = 0;
1043 969 : for (unsigned idx = 0; idx < sdpSession->media_count; idx++) {
1044 628 : mediaList.emplace_back(MediaAttribute {});
1045 628 : auto& mediaAttr = mediaList.back();
1046 :
1047 628 : auto const& media = sdpSession->media[idx];
1048 :
1049 : // Get media type.
1050 628 : if (!pj_stricmp2(&media->desc.media, "audio"))
1051 346 : mediaAttr.type_ = MediaType::MEDIA_AUDIO;
1052 282 : else if (!pj_stricmp2(&media->desc.media, "video"))
1053 282 : mediaAttr.type_ = MediaType::MEDIA_VIDEO;
1054 : else {
1055 0 : JAMI_WARN("Media#%u only 'audio' and 'video' types are supported!", idx);
1056 : // Disable the media. No need to parse the attributes.
1057 0 : mediaAttr.enabled_ = false;
1058 0 : continue;
1059 : }
1060 :
1061 : // Set enabled flag
1062 628 : mediaAttr.enabled_ = media->desc.port > 0;
1063 :
1064 628 : if (!mediaAttr.enabled_ && ignoreDisabled) {
1065 1 : mediaList.pop_back();
1066 1 : continue;
1067 : }
1068 :
1069 : // Get mute state.
1070 627 : auto direction = getMediaDirection(media);
1071 627 : mediaAttr.muted_ = direction != MediaDirection::SENDRECV
1072 627 : and direction != MediaDirection::SENDONLY;
1073 :
1074 : // Get transport.
1075 627 : auto transp = getMediaTransport(media);
1076 627 : if (transp == MediaTransport::UNKNOWN) {
1077 0 : JAMI_WARN("Media#%u could not determine transport type!", idx);
1078 : }
1079 :
1080 : // A media is secure if the transport is of type RTP/SAVP
1081 : // and the crypto materials are present.
1082 627 : mediaAttr.secure_ = transp == MediaTransport::RTP_SAVP and not getCrypto(media).empty();
1083 :
1084 627 : if (mediaAttr.type_ == MediaType::MEDIA_AUDIO) {
1085 346 : mediaAttr.label_ = "audio_" + std::to_string(audioIdx++);
1086 281 : } else if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
1087 281 : mediaAttr.label_ = "video_" + std::to_string(videoIdx++);
1088 : }
1089 : }
1090 :
1091 341 : return mediaList;
1092 341 : }
1093 :
1094 : } // namespace jami
|