Ring Daemon 16.0.0
Loading...
Searching...
No Matches
conference.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2025 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 <regex>
19#include <sstream>
20
21#include "conference.h"
22#include "manager.h"
23#include "audio/audiolayer.h"
24#include "jamidht/jamiaccount.h"
25#include "string_utils.h"
26#include "sip/siptransport.h"
27
28#include "client/videomanager.h"
29#include "tracepoint.h"
30#ifdef ENABLE_VIDEO
31#include "call.h"
32#include "video/video_input.h"
33#include "video/video_mixer.h"
34#endif
35
36#ifdef ENABLE_PLUGIN
38#endif
39
40#include "call_factory.h"
41
42#include "logger.h"
43#include "jami/media_const.h"
45#include "sip/sipcall.h"
46#include "json_utils.h"
47
48#include <opendht/thread_pool.h>
49
50using namespace std::literals;
51
52namespace jami {
53
54Conference::Conference(const std::shared_ptr<Account>& account,
55 const std::string& confId)
56 : id_(confId.empty() ? Manager::instance().callFactory.getNewCallID() : confId)
57 , account_(account)
59 , videoEnabled_(account->isVideoEnabled())
60#endif
61{
62 JAMI_LOG("Create new conference {}", id_);
63 duration_start_ = clock::now();
64
65#ifdef ENABLE_VIDEO
66 videoMixer_ = std::make_shared<video::VideoMixer>(id_);
67 videoMixer_->setOnSourcesUpdated([this](std::vector<video::SourceInfo>&& infos) {
68 runOnMainThread([w = weak(), infos = std::move(infos)] {
69 auto shared = w.lock();
70 if (!shared)
71 return;
72 auto acc = std::dynamic_pointer_cast<JamiAccount>(shared->account_.lock());
73 if (!acc)
74 return;
76 {
77 std::lock_guard lock(shared->confInfoMutex_);
78 newInfo.w = shared->confInfo_.w;
79 newInfo.h = shared->confInfo_.h;
80 newInfo.layout = shared->confInfo_.layout;
81 }
82 auto hostAdded = false;
83 // Handle participants showing their video
84 for (const auto& info : infos) {
85 std::string uri {};
86 bool isLocalMuted = false, isPeerRecording = false;
87 std::string deviceId {};
88 auto active = false;
89 if (!info.callId.empty()) {
90 std::string callId = info.callId;
91 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCall(callId))) {
92 uri = call->getPeerNumber();
93 isLocalMuted = call->isPeerMuted();
94 isPeerRecording = call->isPeerRecording();
95 if (auto* transport = call->getTransport())
96 deviceId = transport->deviceId();
97 }
98 std::string_view peerId = string_remove_suffix(uri, '@');
99 auto isModerator = shared->isModerator(peerId);
100 auto isHandRaised = shared->isHandRaised(deviceId);
101 auto isModeratorMuted = shared->isMuted(callId);
102 auto isVoiceActive = shared->isVoiceActive(info.streamId);
103 if (auto videoMixer = shared->videoMixer_)
104 active = videoMixer->verifyActive(info.streamId);
105 newInfo.emplace_back(ParticipantInfo {std::move(uri),
106 deviceId,
107 std::move(info.streamId),
108 active,
109 info.x,
110 info.y,
111 info.w,
112 info.h,
113 !info.hasVideo,
116 isModerator,
117 isHandRaised,
118 isVoiceActive,
119 isPeerRecording});
120 } else {
121 auto isModeratorMuted = false;
122 // If not local
123 auto streamInfo = shared->videoMixer_->streamInfo(info.source);
124 std::string streamId = streamInfo.streamId;
125 if (!streamId.empty()) {
126 // Retrieve calls participants
127 // TODO: this is a first version, we assume that the peer is not
128 // a master of a conference and there is only one remote
129 // In the future, we should retrieve confInfo from the call
130 // To merge layout information
131 isModeratorMuted = shared->isMuted(streamId);
132 if (auto videoMixer = shared->videoMixer_)
133 active = videoMixer->verifyActive(streamId);
134 if (auto call = std::dynamic_pointer_cast<SIPCall>(
135 getCall(streamInfo.callId))) {
136 uri = call->getPeerNumber();
137 isLocalMuted = call->isPeerMuted();
138 isPeerRecording = call->isPeerRecording();
139 if (auto* transport = call->getTransport())
140 deviceId = transport->deviceId();
141 }
142 } else {
144 if (auto videoMixer = shared->videoMixer_)
145 active = videoMixer->verifyActive(streamId);
146 }
147 std::string_view peerId = string_remove_suffix(uri, '@');
148 auto isModerator = shared->isModerator(peerId);
149 if (uri.empty() && !hostAdded) {
150 hostAdded = true;
151 peerId = "host"sv;
152 deviceId = acc->currentDeviceId();
153 isLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO);
154 isPeerRecording = shared->isRecording();
155 }
156 auto isHandRaised = shared->isHandRaised(deviceId);
157 auto isVoiceActive = shared->isVoiceActive(streamId);
158 newInfo.emplace_back(ParticipantInfo {std::move(uri),
159 deviceId,
160 std::move(streamId),
161 active,
162 info.x,
163 info.y,
164 info.w,
165 info.h,
166 !info.hasVideo,
169 isModerator,
170 isHandRaised,
171 isVoiceActive,
172 isPeerRecording});
173 }
174 }
175 if (auto videoMixer = shared->videoMixer_) {
176 newInfo.h = videoMixer->getHeight();
177 newInfo.w = videoMixer->getWidth();
178 }
179 if (!hostAdded) {
181 pi.videoMuted = true;
182 pi.audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO);
183 pi.isModerator = true;
184 newInfo.emplace_back(pi);
185 }
186
187 shared->updateConferenceInfo(std::move(newInfo));
188 });
189 });
191 .videoPreferences.getConferenceResolution(),
192 'x');
193 if (conf_res.size() == 2u) {
194#if defined(__APPLE__) && TARGET_OS_MAC
195 videoMixer_->setParameters(conf_res[0], conf_res[1], AV_PIX_FMT_NV12);
196#else
197 videoMixer_->setParameters(conf_res[0], conf_res[1]);
198#endif
199 } else {
200 JAMI_ERR("Conference resolution is invalid");
201 }
202#endif
203
204 parser_.onVersion([&](uint32_t) {}); // TODO
205 parser_.onCheckAuthorization([&](std::string_view peerId) { return isModerator(peerId); });
206 parser_.onHangupParticipant([&](const auto& accountUri, const auto& deviceId) {
207 hangupParticipant(accountUri, deviceId);
208 });
209 parser_.onRaiseHand([&](const auto& deviceId, bool state) { setHandRaised(deviceId, state); });
210 parser_.onSetActiveStream(
211 [&](const auto& streamId, bool state) { setActiveStream(streamId, state); });
212 parser_.onMuteStreamAudio(
213 [&](const auto& accountUri, const auto& deviceId, const auto& streamId, bool state) {
214 muteStream(accountUri, deviceId, streamId, state);
215 });
216 parser_.onSetLayout([&](int layout) { setLayout(layout); });
217
218 // Version 0, deprecated
219 parser_.onKickParticipant([&](const auto& participantId) { hangupParticipant(participantId); });
221 [&](const auto& participantId) { setActiveParticipant(participantId); });
222 parser_.onMuteParticipant(
223 [&](const auto& participantId, bool state) { muteParticipant(participantId, state); });
224 parser_.onRaiseHandUri([&](const auto& uri, bool state) {
225 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCallFromPeerID(uri)))
226 if (auto* transport = call->getTransport())
227 setHandRaised(std::string(transport->deviceId()), state);
228 });
229
230 parser_.onVoiceActivity(
231 [&](const auto& streamId, bool state) { setVoiceActivity(streamId, state); });
232 jami_tracepoint(conference_begin, id_.c_str());
233}
234
236{
237 JAMI_INFO("Destroying conference %s", id_.c_str());
238
239#ifdef ENABLE_VIDEO
241 auto defaultDevice = videoManager ? videoManager->videoDeviceMonitor.getMRLForDefaultDevice()
242 : std::string {};
243 foreachCall([&](auto call) {
244 call->exitConference();
245 // Reset distant callInfo
246 call->resetConfInfo();
247 // Trigger the SIP negotiation to update the resolution for the remaining call
248 // ideally this sould be done without renegotiation
249 call->switchInput(defaultDevice);
250
251 // Continue the recording for the call if the conference was recorded
252 if (isRecording()) {
253 JAMI_DEBUG("Stop recording for conf {:s}", getConfId());
255 if (not call->isRecording()) {
256 JAMI_DEBUG("Conference was recorded, start recording for conf {:s}",
257 call->getCallId());
258 call->toggleRecording();
259 }
260 }
261 // Notify that the remaining peer is still recording after conference
262 if (call->isPeerRecording())
263 call->peerRecording(true);
264 });
265 if (videoMixer_) {
266 auto& sink = videoMixer_->getSink();
267 for (auto it = confSinksMap_.begin(); it != confSinksMap_.end();) {
268 sink->detach(it->second.get());
269 it->second->stop();
270 it = confSinksMap_.erase(it);
271 }
272 }
273#endif // ENABLE_VIDEO
274#ifdef ENABLE_PLUGIN
275 {
276 std::lock_guard lk(avStreamsMtx_);
278 .getJamiPluginManager()
279 .getCallServicesManager()
280 .clearCallHandlerMaps(getConfId());
281 Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject(
282 getConfId());
283 confAVStreams.clear();
284 }
285#endif // ENABLE_PLUGIN
286 if (shutdownCb_)
287 shutdownCb_(getDuration().count());
288 // do not propagate sharing from conf host to calls
289 closeMediaPlayer(mediaPlayerId_);
290 jami_tracepoint(conference_end, id_.c_str());
291}
292
295{
296 return confState_;
297}
298
299void
301{
302 JAMI_DEBUG("[conf {:s}] Set state to [{:s}] (was [{:s}])",
303 id_,
304 getStateStr(state),
305 getStateStr());
306
307 confState_ = state;
308}
309
310void
311Conference::initSourcesForHost()
312{
313 hostSources_.clear();
314 // Setup local audio source
316 if (confState_ == State::ACTIVE_ATTACHED) {
319 }
320
321 JAMI_DEBUG("[conf {:s}] Setting local host audio source to [{:s}]", id_, audioAttr.toString());
322 hostSources_.emplace_back(audioAttr);
323
324#ifdef ENABLE_VIDEO
325 if (isVideoEnabled()) {
326 MediaAttribute videoAttr;
327 // Setup local video source
328 if (confState_ == State::ACTIVE_ATTACHED) {
331 false,
332 false,
333 true,
334 Manager::instance().getVideoManager()->videoDeviceMonitor.getMRLForDefaultDevice(),
336 }
337 JAMI_DEBUG("[conf {:s}] Setting local host video source to [{:s}]",
338 id_,
339 videoAttr.toString());
340 hostSources_.emplace_back(videoAttr);
341 }
342#endif
343
345}
346
347void
355
356std::vector<std::map<std::string, std::string>>
361
362#ifdef ENABLE_PLUGIN
363void
364Conference::createConfAVStreams()
365{
366 std::string accountId = getAccountId();
367
368 auto audioMap = [](const std::shared_ptr<jami::MediaFrame>& m) -> AVFrame* {
369 return std::static_pointer_cast<AudioFrame>(m)->pointer();
370 };
371
372 // Preview and Received
373 if ((audioMixer_ = jami::getAudioInput(getConfId()))) {
374 auto audioSubject = std::make_shared<MediaStreamSubject>(audioMap);
379 }
380
381#ifdef ENABLE_VIDEO
382
383 if (videoMixer_) {
384 // Review
385 auto receiveSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_);
388
389 // Preview
390 if (auto videoPreview = videoMixer_->getVideoLocal()) {
391 auto previewSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_);
393 false,
395 getConfId(),
396 accountId};
398 }
399 }
400#endif // ENABLE_VIDEO
401}
402
403void
404Conference::createConfAVStream(const StreamData& StreamData,
406 const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject,
407 bool force)
408{
409 std::lock_guard lk(avStreamsMtx_);
410 const std::string AVStreamId = StreamData.id + std::to_string(static_cast<int>(StreamData.type))
411 + std::to_string(StreamData.direction);
412 auto it = confAVStreams.find(AVStreamId);
413 if (!force && it != confAVStreams.end())
414 return;
415
418 streamSource.attachPriorityObserver(mediaStreamSubject);
420 .getJamiPluginManager()
421 .getCallServicesManager()
422 .createAVSubject(StreamData, mediaStreamSubject);
423}
424#endif // ENABLE_PLUGIN
425
426void
428{
429 for (auto& source : hostSources_)
430 if (source.type_ == type)
431 source.muted_ = muted;
432}
433
434bool
436{
438 // Assume muted if not attached.
439 return true;
440 }
441
443 JAMI_ERR("Unsupported media type");
444 return true;
445 }
446
447 // if one is muted, then consider that all are
448 for (const auto& source : hostSources_) {
449 if (source.muted_ && source.type_ == type)
450 return true;
451 if (source.type_ == MediaType::MEDIA_NONE) {
452 JAMI_WARN("The host source for %s is not set. The mute state is meaningless",
453 source.mediaTypeToString(source.type_));
454 // Assume muted if the media is not present.
455 return true;
456 }
457 }
458 return false;
459}
460
461void
462Conference::takeOverMediaSourceControl(const std::string& callId)
463{
464 auto call = getCall(callId);
465 if (not call) {
466 JAMI_ERR("No call matches participant %s", callId.c_str());
467 return;
468 }
469
470 auto account = call->getAccount().lock();
471 if (not account) {
472 JAMI_ERR("No account detected for call %s", callId.c_str());
473 return;
474 }
475
476 auto mediaList = call->getMediaAttributeList();
477
479
480 for (auto mediaType : mediaTypeList) {
481 // Try to find a media with a valid source type
482 auto check = [mediaType](auto const& mediaAttr) {
483 return (mediaAttr.type_ == mediaType);
484 };
485
486 auto iter = std::find_if(mediaList.begin(), mediaList.end(), check);
487
488 if (iter == mediaList.end()) {
489 // Nothing to do if the call does not have a stream with
490 // the requested media.
491 JAMI_DEBUG("[Call: {:s}] Does not have an active [{:s}] media source",
492 callId,
494 continue;
495 }
496
498 // To mute the local source, all the sources of the participating
499 // calls must be muted. If it's the first participant, just use
500 // its mute state.
501 if (subCalls_.size() == 1) {
502 setLocalHostMuteState(iter->type_, iter->muted_);
503 } else {
504 setLocalHostMuteState(iter->type_, iter->muted_ or isMediaSourceMuted(iter->type_));
505 }
506 }
507 }
508
509 // Update the media states in the newly added call.
510 call->requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList));
511
512 // Notify the client
513 for (auto mediaType : mediaTypeList) {
514 if (mediaType == MediaType::MEDIA_AUDIO) {
516 JAMI_WARN("Take over [AUDIO] control from call %s - current local source state [%s]",
517 callId.c_str(),
518 muted ? "muted" : "un-muted");
520 } else {
522 JAMI_WARN("Take over [VIDEO] control from call %s - current local source state [%s]",
523 callId.c_str(),
524 muted ? "muted" : "un-muted");
526 }
527 }
528}
529
530bool
531Conference::requestMediaChange(const std::vector<libjami::MediaMap>& mediaList)
532{
534 JAMI_ERROR("[conf {}] Request media change can be performed only in attached mode",
535 getConfId());
536 return false;
537 }
538
539 JAMI_DEBUG("[conf {:s}] Request media change", getConfId());
540
542
543 bool hasFileSharing {false};
544 for (const auto& media : mediaAttrList) {
545 if (!media.enabled_ || media.sourceUri_.empty())
546 continue;
547
548 // Supported MRL schemes
549 static const std::string sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
550
551 const auto pos = media.sourceUri_.find(sep);
552 if (pos == std::string::npos)
553 continue;
554
555 const auto prefix = media.sourceUri_.substr(0, pos);
556 if ((pos + sep.size()) >= media.sourceUri_.size())
557 continue;
558
560 hasFileSharing = true;
561 mediaPlayerId_ = media.sourceUri_;
562 createMediaPlayer(mediaPlayerId_);
563 }
564 }
565
566 if (!hasFileSharing) {
567 closeMediaPlayer(mediaPlayerId_);
568 mediaPlayerId_ = "";
569 }
570
571 for (auto const& mediaAttr : mediaAttrList) {
572 JAMI_DEBUG("[conf {:s}] New requested media: {:s}", getConfId(), mediaAttr.toString(true));
573 }
574
575 std::vector<std::string> newVideoInputs;
576 for (auto const& mediaAttr : mediaAttrList) {
577 // Find media
578 auto oldIdx = std::find_if(hostSources_.begin(), hostSources_.end(), [&](auto oldAttr) {
579 return oldAttr.label_ == mediaAttr.label_;
580 });
581 // If video, add to newVideoInputs
582#ifdef ENABLE_VIDEO
583 if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
584 auto srcUri = mediaAttr.sourceUri_;
585 // If no sourceUri, use the default video device
586 if (srcUri.empty()) {
587 if (auto vm = Manager::instance().getVideoManager())
588 srcUri = vm->videoDeviceMonitor.getMRLForDefaultDevice();
589 else
590 continue;
591 }
592 if (!mediaAttr.muted_)
593 newVideoInputs.emplace_back(std::move(srcUri));
594 } else {
595#endif
596 hostAudioInputs_[mediaAttr.label_] = jami::getAudioInput(mediaAttr.label_);
597#ifdef ENABLE_VIDEO
598 }
599#endif
600 if (oldIdx != hostSources_.end()) {
601 // Check if muted status changes
602 if (mediaAttr.muted_ != oldIdx->muted_) {
603 // If the current media source is muted, just call un-mute, it
604 // will set the new source as input.
609 }
610 }
611 }
612
613#ifdef ENABLE_VIDEO
614 if (videoMixer_) {
615 if (newVideoInputs.empty()) {
616 videoMixer_->addAudioOnlySource("", sip_utils::streamId("", sip_utils::DEFAULT_AUDIO_STREAMID));
617 } else {
618 videoMixer_->switchInputs(newVideoInputs);
619 }
620 }
621#endif
622 hostSources_ = mediaAttrList; // New medias
623 if (!isMuted("host"sv) && !isMediaSourceMuted(MediaType::MEDIA_AUDIO))
624 bindHostAudio();
625
626 // It's host medias, so no need to negotiate anything, but inform the client.
628 return true;
629}
630
631void
632Conference::handleMediaChangeRequest(const std::shared_ptr<Call>& call,
633 const std::vector<libjami::MediaMap>& remoteMediaList)
634{
635 JAMI_DEBUG("Conf [{:s}] Answer to media change request", getConfId());
636 auto currentMediaList = hostSources_;
637
638#ifdef ENABLE_VIDEO
639 // If the new media list has video, remove the participant from audioonlylist.
640 auto remoteHasVideo
642 false),
644 if (videoMixer_ && remoteHasVideo) {
645 auto callId = call->getCallId();
646 videoMixer_->removeAudioOnlySource(
647 callId, std::string(sip_utils::streamId(callId, sip_utils::DEFAULT_AUDIO_STREAMID)));
648 }
649#endif
650
652 for (auto it = remoteList.begin(); it != remoteList.end();) {
655 it = remoteList.erase(it);
656 } else {
657 ++it;
658 }
659 }
660 // Create minimum media list (ignore muted and disabled medias)
661 std::vector<libjami::MediaMap> newMediaList;
662 newMediaList.reserve(remoteMediaList.size());
663 for (auto const& media : currentMediaList) {
664 if (media.enabled_ and not media.muted_)
666 }
667 for (auto idx = newMediaList.size(); idx < remoteMediaList.size(); idx++)
668 newMediaList.emplace_back(remoteMediaList[idx]);
669
670 // NOTE:
671 // Since this is a conference, newly added media will be also
672 // accepted.
673 // This also means that if original call was an audio-only call,
674 // the local camera will be enabled, unless the video is disabled
675 // in the account settings.
676 call->answerMediaChangeRequest(newMediaList);
677 call->enterConference(shared_from_this());
678}
679
680void
681Conference::addSubCall(const std::string& callId)
682{
683 JAMI_DEBUG("Adding call {:s} to conference {:s}", callId, id_);
684
685
686 jami_tracepoint(conference_add_participant, id_.c_str(), callId.c_str());
687
688 {
689 std::lock_guard lk(subcallsMtx_);
690 if (!subCalls_.insert(callId).second)
691 return;
692 }
693
694 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCall(callId))) {
695 // Check if participant was muted before conference
696 if (call->isPeerMuted())
697 participantsMuted_.emplace(call->getCallId());
698
699 // NOTE:
700 // When a call joins a conference, the media source of the call
701 // will be set to the output of the conference mixer.
702 takeOverMediaSourceControl(callId);
703 auto w = call->getAccount();
704 auto account = w.lock();
705 if (account) {
706 // Add defined moderators for the account link to the call
707 for (const auto& mod : account->getDefaultModerators()) {
708 moderators_.emplace(mod);
709 }
710
711 // Check for localModeratorsEnabled preference
712 if (account->isLocalModeratorsEnabled() && not localModAdded_) {
714 for (const auto& account : accounts) {
715 moderators_.emplace(account->getUsername());
716 }
717 localModAdded_ = true;
718 }
719
720 // Check for allModeratorEnabled preference
721 if (account->isAllModerators())
722 moderators_.emplace(getRemoteId(call));
723 }
724#ifdef ENABLE_VIDEO
725 // In conference, if a participant joins with an audio only
726 // call, it must be listed in the audioonlylist.
727 auto mediaList = call->getMediaAttributeList();
728 if (call->peerUri().find("swarm:") != 0) { // We're hosting so it's already ourself.
730 videoMixer_->addAudioOnlySource(call->getCallId(),
731 sip_utils::streamId(call->getCallId(),
733 }
734 }
735 call->enterConference(shared_from_this());
736 // Continue the recording for the conference if one participant was recording
737 if (call->isRecording()) {
738 JAMI_DEBUG("Stop recording for call {:s}", call->getCallId());
739 call->toggleRecording();
740 if (not this->isRecording()) {
741 JAMI_DEBUG("One participant was recording, start recording for conference {:s}",
742 getConfId());
743 this->toggleRecording();
744 }
745 }
746 bindSubCallAudio(callId);
747#endif // ENABLE_VIDEO
748 } else
749 JAMI_ERROR("no call associate to participant {}", callId.c_str());
750#ifdef ENABLE_PLUGIN
752#endif
753}
754
755void
756Conference::removeSubCall(const std::string& callId)
757{
758 JAMI_DEBUG("Remove call {:s} in conference {:s}", callId, id_);
759 {
760 std::lock_guard lk(subcallsMtx_);
761 if (!subCalls_.erase(callId))
762 return;
763 }
764 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCall(callId))) {
765 const auto& peerId = getRemoteId(call);
766 participantsMuted_.erase(call->getCallId());
767 if (auto* transport = call->getTransport())
768 handsRaised_.erase(std::string(transport->deviceId()));
769#ifdef ENABLE_VIDEO
770 if (videoMixer_) {
771 for (auto const& rtpSession : call->getRtpSessionList()) {
772 if (rtpSession->getMediaType() == MediaType::MEDIA_AUDIO)
773 videoMixer_->removeAudioOnlySource(callId, rtpSession->streamId());
774 if (videoMixer_->verifyActive(rtpSession->streamId()))
775 videoMixer_->resetActiveStream();
776 }
777 }
778
779 auto sinkId = getConfId() + peerId;
780 unbindSubCallAudio(callId);
781 call->exitConference();
782 if (call->isPeerRecording())
783 call->peerRecording(false);
784#endif // ENABLE_VIDEO
785 }
786}
787
788void
790{
791#ifdef ENABLE_VIDEO
792 if (!videoMixer_)
793 return;
794 if (isHost(participant_id)) {
795 videoMixer_->setActiveStream(sip_utils::streamId("", sip_utils::DEFAULT_VIDEO_STREAMID));
796 return;
797 }
798 if (auto call = getCallFromPeerID(participant_id)) {
799 videoMixer_->setActiveStream(
801 return;
802 }
803
804 auto remoteHost = findHostforRemoteParticipant(participant_id);
805 if (not remoteHost.empty()) {
806 // This logic will be handled client side
807 JAMI_WARN("Change remote layout is not supported");
808 return;
809 }
810 // Unset active participant by default
811 videoMixer_->resetActiveStream();
812#endif
813}
814
815void
816Conference::setActiveStream(const std::string& streamId, bool state)
817{
818#ifdef ENABLE_VIDEO
819 if (!videoMixer_)
820 return;
821 if (state)
822 videoMixer_->setActiveStream(streamId);
823 else
824 videoMixer_->resetActiveStream();
825#endif
826}
827
828void
830{
831#ifdef ENABLE_VIDEO
833 JAMI_ERR("Unknown layout %u", layout);
834 return;
835 }
836 if (!videoMixer_)
837 return;
838 {
839 std::lock_guard lk(confInfoMutex_);
840 confInfo_.layout = layout;
841 }
842 videoMixer_->setVideoLayout(static_cast<video::Layout>(layout));
843#endif
844}
845
846std::vector<std::map<std::string, std::string>>
848{
849 std::vector<std::map<std::string, std::string>> infos;
850 infos.reserve(size());
851 for (const auto& info : *this)
852 infos.emplace_back(info.toMap());
853 return infos;
854}
855
856std::string
858{
859 Json::Value val = {};
860 for (const auto& info : *this) {
861 val["p"].append(info.toJson());
862 }
863 val["w"] = w;
864 val["h"] = h;
865 val["v"] = v;
866 val["layout"] = layout;
867 return json::toString(val);
868}
869
870void
871Conference::sendConferenceInfos()
872{
873 // Inform calls that the layout has changed
874 foreachCall([&](auto call) {
875 // Produce specific JSON for each participant (2 separate accounts can host ...
876 // a conference on a same device, the conference is not link to one account).
877 auto w = call->getAccount();
878 auto account = w.lock();
879 if (!account)
880 return;
881
882 dht::ThreadPool::io().run(
883 [call,
884 confInfo = getConfInfoHostUri(account->getUsername() + "@ring.dht",
885 call->getPeerNumber())] {
886 call->sendConfInfo(confInfo.toString());
887 });
888 });
889
890 auto confInfo = getConfInfoHostUri("", "");
891#ifdef ENABLE_VIDEO
893#endif
894
895
896 // Inform client that layout has changed
899 .toVectorMapStringString());
900}
901
902#ifdef ENABLE_VIDEO
903void
904Conference::createSinks(const ConfInfo& infos)
905{
906 std::lock_guard lk(sinksMtx_);
907 if (!videoMixer_)
908 return;
909 auto& sink = videoMixer_->getSink();
910 Manager::instance().createSinkClients(getConfId(),
911 infos,
912 {std::static_pointer_cast<video::VideoFrameActiveWriter>(
913 sink)},
915}
916#endif
917
918void
919Conference::attachHost(const std::vector<libjami::MediaMap>& mediaList)
920{
921 JAMI_LOG("Attach local participant to conference {}", id_);
922
925 if (mediaList.empty()) {
926 initSourcesForHost();
927 bindHostAudio();
928#ifdef ENABLE_VIDEO
929 if (videoMixer_) {
930 std::vector<std::string> videoInputs;
931 for (const auto& source : hostSources_) {
932 if (source.type_ == MediaType::MEDIA_VIDEO)
933 videoInputs.emplace_back(source.sourceUri_);
934 }
935 if (videoInputs.empty()) {
936 videoMixer_->addAudioOnlySource("", sip_utils::streamId("", sip_utils::DEFAULT_AUDIO_STREAMID));
937 } else {
938 videoMixer_->switchInputs(videoInputs);
939 }
940 }
941#endif
942 } else {
944 }
945 } else {
947 "Invalid conference state in attach participant: current \"{}\" - expected \"{}\"",
948 getStateStr(),
949 "ACTIVE_DETACHED");
950 }
951}
952
953void
955{
956 JAMI_LOG("Detach local participant from conference {}", id_);
957
959 unbindHostAudio();
960
961#ifdef ENABLE_VIDEO
962 if (videoMixer_)
963 videoMixer_->stopInputs();
964#endif
965 } else {
967 "Invalid conference state in detach participant: current \"{}\" - expected \"{}\"",
968 getStateStr(),
969 "ACTIVE_ATTACHED");
970 return;
971 }
972
974 initSourcesForHost();
975}
976
979{
980 std::lock_guard lk(subcallsMtx_);
981 return subCalls_;
982}
983
984bool
986{
987 bool newState = not isRecording();
988 if (newState)
989 initRecorder(recorder_);
990 else if (recorder_)
991 deinitRecorder(recorder_);
992
993 // Notify each participant
994 foreachCall([&](auto call) { call->updateRecState(newState); });
995
998 return res;
999}
1000
1001std::string
1003{
1004 if (auto account = getAccount())
1005 return account->getAccountID();
1006 return {};
1007}
1008
1009void
1010Conference::switchInput(const std::string& input)
1011{
1012#ifdef ENABLE_VIDEO
1013 JAMI_DEBUG("[Conf:{:s}] Setting video input to {:s}", id_, input);
1014 std::vector<MediaAttribute> newSources;
1015 auto firstVideo = true;
1016 // Rewrite hostSources (remove all except one video input)
1017 // This method is replaced by requestMediaChange
1018 for (auto& source : hostSources_) {
1019 if (source.type_ == MediaType::MEDIA_VIDEO) {
1020 if (firstVideo) {
1021 firstVideo = false;
1022 source.sourceUri_ = input;
1023 newSources.emplace_back(source);
1024 }
1025 } else {
1026 newSources.emplace_back(source);
1027 }
1028 }
1029
1030 // Done if the video is disabled
1031 if (not isVideoEnabled())
1032 return;
1033
1034 if (auto mixer = videoMixer_) {
1035 mixer->switchInputs({input});
1036#ifdef ENABLE_PLUGIN
1037 // Preview
1038 if (auto videoPreview = mixer->getVideoLocal()) {
1039 auto previewSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_);
1041 false,
1043 getConfId(),
1044 getAccountId()};
1046 }
1047#endif
1048 }
1049#endif
1050}
1051
1052bool
1054{
1055 if (auto shared = account_.lock())
1056 return shared->isVideoEnabled();
1057 return false;
1058}
1059
1060#ifdef ENABLE_VIDEO
1061std::shared_ptr<video::VideoMixer>
1062Conference::getVideoMixer()
1063{
1064 return videoMixer_;
1065}
1066
1067std::string
1068Conference::getVideoInput() const
1069{
1070 for (const auto& source : hostSources_) {
1071 if (source.type_ == MediaType::MEDIA_VIDEO)
1072 return source.sourceUri_;
1073 }
1074 return {};
1075}
1076#endif
1077
1078void
1079Conference::initRecorder(std::shared_ptr<MediaRecorder>& rec)
1080{
1081#ifdef ENABLE_VIDEO
1082 // Video
1083 if (videoMixer_) {
1084 if (auto ob = rec->addStream(videoMixer_->getStream("v:mixer"))) {
1085 videoMixer_->attach(ob);
1086 }
1087 }
1088#endif
1089
1090 // Audio
1091 // Create ghost participant for ringbufferpool
1093 ghostRingBuffer_ = rbPool.createRingBuffer(getConfId());
1094
1095 // Bind it to ringbufferpool in order to get the all mixed frames
1096 bindSubCallAudio(getConfId());
1097
1098 // Add stream to recorder
1099 audioMixer_ = jami::getAudioInput(getConfId());
1100 if (auto ob = rec->addStream(audioMixer_->getInfo("a:mixer"))) {
1101 audioMixer_->attach(ob);
1102 }
1103}
1104
1105void
1106Conference::deinitRecorder(std::shared_ptr<MediaRecorder>& rec)
1107{
1108#ifdef ENABLE_VIDEO
1109 // Video
1110 if (videoMixer_) {
1111 if (auto ob = rec->getStream("v:mixer")) {
1112 videoMixer_->detach(ob);
1113 }
1114 }
1115#endif
1116
1117 // Audio
1118 if (auto ob = rec->getStream("a:mixer"))
1119 audioMixer_->detach(ob);
1120 audioMixer_.reset();
1122 ghostRingBuffer_.reset();
1123}
1124
1125void
1126Conference::onConfOrder(const std::string& callId, const std::string& confOrder)
1127{
1128 // Check if the peer is a master
1129 if (auto call = getCall(callId)) {
1130 const auto& peerId = getRemoteId(call);
1131 Json::Value root;
1132 if (!json::parse(confOrder, root)) {
1133 JAMI_WARNING("Unable to parse conference order from {}", peerId);
1134 return;
1135 }
1136
1137 parser_.initData(std::move(root), peerId);
1138 parser_.parse();
1139 }
1140}
1141
1142std::shared_ptr<Call>
1143Conference::getCall(const std::string& callId)
1144{
1145 return Manager::instance().callFactory.getCall(callId);
1146}
1147
1148bool
1149Conference::isModerator(std::string_view uri) const
1150{
1151 return moderators_.find(uri) != moderators_.end() or isHost(uri);
1152}
1153
1154bool
1155Conference::isHandRaised(std::string_view deviceId) const
1156{
1157 return isHostDevice(deviceId) ? handsRaised_.find("host"sv) != handsRaised_.end()
1158 : handsRaised_.find(deviceId) != handsRaised_.end();
1159}
1160
1161void
1162Conference::setHandRaised(const std::string& deviceId, const bool& state)
1163{
1164 if (isHostDevice(deviceId)) {
1165 auto isPeerRequiringAttention = isHandRaised("host"sv);
1166 if (state and not isPeerRequiringAttention) {
1167 JAMI_DBG("Raise host hand");
1168 handsRaised_.emplace("host"sv);
1169 updateHandsRaised();
1170 } else if (not state and isPeerRequiringAttention) {
1171 JAMI_DBG("Lower host hand");
1172 handsRaised_.erase("host");
1173 updateHandsRaised();
1174 }
1175 } else {
1176 for (const auto& p : getSubCalls()) {
1177 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCall(p))) {
1178 auto isPeerRequiringAttention = isHandRaised(deviceId);
1179 std::string callDeviceId;
1180 if (auto* transport = call->getTransport())
1181 callDeviceId = transport->deviceId();
1182 if (deviceId == callDeviceId) {
1183 if (state and not isPeerRequiringAttention) {
1184 JAMI_DEBUG("Raise {:s} hand", deviceId);
1185 handsRaised_.emplace(deviceId);
1186 updateHandsRaised();
1187 } else if (not state and isPeerRequiringAttention) {
1188 JAMI_DEBUG("Remove {:s} raised hand", deviceId);
1189 handsRaised_.erase(deviceId);
1190 updateHandsRaised();
1191 }
1192 return;
1193 }
1194 }
1195 }
1196 JAMI_WARN("Fail to raise %s hand (participant not found)", deviceId.c_str());
1197 }
1198}
1199
1200bool
1201Conference::isVoiceActive(std::string_view streamId) const
1202{
1203 return streamsVoiceActive.find(streamId) != streamsVoiceActive.end();
1204}
1205
1206void
1207Conference::setVoiceActivity(const std::string& streamId, const bool& newState)
1208{
1209 // verify that streamID exists in our confInfo
1210 bool exists = false;
1211 for (auto& participant : confInfo_) {
1212 if (participant.sinkId == streamId) {
1213 exists = true;
1214 break;
1215 }
1216 }
1217
1218 if (!exists) {
1219 JAMI_ERR("participant not found with streamId: %s", streamId.c_str());
1220 return;
1221 }
1222
1223 auto previousState = isVoiceActive(streamId);
1224
1225 if (previousState == newState) {
1226 // no change, do not send out updates
1227 return;
1228 }
1229
1231 // voice going from inactive to active
1232 streamsVoiceActive.emplace(streamId);
1234 return;
1235 }
1236
1238 // voice going from active to inactive
1239 streamsVoiceActive.erase(streamId);
1241 return;
1242 }
1243}
1244
1245void
1246Conference::setModerator(const std::string& participant_id, const bool& state)
1247{
1248 for (const auto& p : getSubCalls()) {
1249 if (auto call = getCall(p)) {
1250 auto isPeerModerator = isModerator(participant_id);
1251 if (participant_id == getRemoteId(call)) {
1252 if (state and not isPeerModerator) {
1253 JAMI_DEBUG("Add {:s} as moderator", participant_id);
1254 moderators_.emplace(participant_id);
1255 updateModerators();
1256 } else if (not state and isPeerModerator) {
1257 JAMI_DEBUG("Remove {:s} as moderator", participant_id);
1258 moderators_.erase(participant_id);
1259 updateModerators();
1260 }
1261 return;
1262 }
1263 }
1264 }
1265 JAMI_WARN("Fail to set %s as moderator (participant not found)", participant_id.c_str());
1266}
1267
1268void
1269Conference::updateModerators()
1270{
1271 std::lock_guard lk(confInfoMutex_);
1272 for (auto& info : confInfo_) {
1273 info.isModerator = isModerator(string_remove_suffix(info.uri, '@'));
1274 }
1275 sendConferenceInfos();
1276}
1277
1278void
1279Conference::updateHandsRaised()
1280{
1281 std::lock_guard lk(confInfoMutex_);
1282 for (auto& info : confInfo_)
1283 info.handRaised = isHandRaised(info.device);
1284 sendConferenceInfos();
1285}
1286
1287void
1289{
1290 std::lock_guard lk(confInfoMutex_);
1291
1292 // streamId is actually sinkId
1293 for (ParticipantInfo& participantInfo : confInfo_) {
1294 bool newActivity;
1295
1296 if (auto call = getCallWith(std::string(string_remove_suffix(participantInfo.uri, '@')),
1297 participantInfo.device)) {
1298 // if this participant is in a direct call with us
1299 // grab voice activity info directly from the call
1300 newActivity = call->hasPeerVoice();
1301 } else {
1302 // check for it
1303 newActivity = isVoiceActive(participantInfo.sinkId);
1304 }
1305
1306 if (participantInfo.voiceActivity != newActivity) {
1307 participantInfo.voiceActivity = newActivity;
1308 }
1309 }
1310 sendConferenceInfos(); // also emits signal to client
1311}
1312
1313void
1314Conference::foreachCall(const std::function<void(const std::shared_ptr<Call>& call)>& cb)
1315{
1316 for (const auto& p : getSubCalls())
1317 if (auto call = getCall(p))
1318 cb(call);
1319}
1320
1321bool
1322Conference::isMuted(std::string_view callId) const
1323{
1324 return participantsMuted_.find(callId) != participantsMuted_.end();
1325}
1326
1327void
1329 const std::string& deviceId,
1330 const std::string&,
1331 const bool& state)
1332{
1333 if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account_.lock())) {
1334 if (accountUri == acc->getUsername() && deviceId == acc->currentDeviceId()) {
1335 muteHost(state);
1336 } else if (auto call = getCallWith(accountUri, deviceId)) {
1337 muteCall(call->getCallId(), state);
1338 } else {
1339 JAMI_WARN("No call with %s - %s", accountUri.c_str(), deviceId.c_str());
1340 }
1341 }
1342}
1343
1344void
1345Conference::muteHost(bool state)
1346{
1347 auto isHostMuted = isMuted("host"sv);
1348 if (state and not isHostMuted) {
1349 participantsMuted_.emplace("host"sv);
1351 JAMI_DBG("Mute host");
1352 unbindHostAudio();
1353 }
1354 } else if (not state and isHostMuted) {
1355 participantsMuted_.erase("host");
1357 JAMI_DBG("Unmute host");
1358 bindHostAudio();
1359 }
1360 }
1361 updateMuted();
1362}
1363
1364void
1365Conference::muteCall(const std::string& callId, bool state)
1366{
1367 auto isPartMuted = isMuted(callId);
1368 if (state and not isPartMuted) {
1369 JAMI_DEBUG("Mute participant {:s}", callId);
1370 participantsMuted_.emplace(callId);
1371 unbindSubCallAudio(callId);
1372 updateMuted();
1373 } else if (not state and isPartMuted) {
1374 JAMI_DEBUG("Unmute participant {:s}", callId);
1375 participantsMuted_.erase(callId);
1376 bindSubCallAudio(callId);
1377 updateMuted();
1378 }
1379}
1380
1381void
1382Conference::muteParticipant(const std::string& participant_id, const bool& state)
1383{
1384 // Prioritize remote mute, otherwise the mute info is lost during
1385 // the conference merge (we don't send back info to remoteHost,
1386 // cf. getConfInfoHostUri method)
1387
1388 // Transfert remote participant mute
1389 auto remoteHost = findHostforRemoteParticipant(participant_id);
1390 if (not remoteHost.empty()) {
1391 if (auto call = getCallFromPeerID(string_remove_suffix(remoteHost, '@'))) {
1392 auto w = call->getAccount();
1393 auto account = w.lock();
1394 if (!account)
1395 return;
1396 Json::Value root;
1397 root["muteParticipant"] = participant_id;
1398 root["muteState"] = state ? TRUE_STR : FALSE_STR;
1399 call->sendConfOrder(root);
1400 return;
1401 }
1402 }
1403
1404 // NOTE: For now we have no way to mute only one stream
1405 if (isHost(participant_id))
1406 muteHost(state);
1407 else if (auto call = getCallFromPeerID(participant_id))
1408 muteCall(call->getCallId(), state);
1409}
1410
1411void
1413{
1414 std::lock_guard lk(confInfoMutex_);
1415 for (auto& info : confInfo_) {
1416 if (info.uri.empty()) {
1417 info.recording = isRecording();
1418 } else if (auto call = getCallWith(std::string(string_remove_suffix(info.uri, '@')),
1419 info.device)) {
1420 info.recording = call->isPeerRecording();
1421 }
1422 }
1423 sendConferenceInfos();
1424}
1425
1426void
1428{
1429 std::lock_guard lk(confInfoMutex_);
1430 for (auto& info : confInfo_) {
1431 if (info.uri.empty()) {
1432 info.audioModeratorMuted = isMuted("host"sv);
1433 info.audioLocalMuted = isMediaSourceMuted(MediaType::MEDIA_AUDIO);
1434 } else if (auto call = getCallWith(std::string(string_remove_suffix(info.uri, '@')),
1435 info.device)) {
1436 info.audioModeratorMuted = isMuted(call->getCallId());
1437 info.audioLocalMuted = call->isPeerMuted();
1438 }
1439 }
1440 sendConferenceInfos();
1441}
1442
1444Conference::getConfInfoHostUri(std::string_view localHostURI, std::string_view destURI)
1445{
1446 ConfInfo newInfo = confInfo_;
1447
1448 for (auto it = newInfo.begin(); it != newInfo.end();) {
1449 bool isRemoteHost = remoteHosts_.find(it->uri) != remoteHosts_.end();
1450 if (it->uri.empty() and not destURI.empty()) {
1451 // fill the empty uri with the local host URI, let void for local client
1452 it->uri = localHostURI;
1453 // If we're detached, remove the host
1455 it = newInfo.erase(it);
1456 continue;
1457 }
1458 }
1459 if (isRemoteHost) {
1460 // Don't send back the ParticipantInfo for remote Host
1461 // For other than remote Host, the new info is in remoteHosts_
1462 it = newInfo.erase(it);
1463 } else {
1464 ++it;
1465 }
1466 }
1467 // Add remote Host info
1468 for (const auto& [hostUri, confInfo] : remoteHosts_) {
1469 // Add remote info for remote host destination
1470 // Example: ConfA, ConfB & ConfC
1471 // ConfA send ConfA and ConfB for ConfC
1472 // ConfA send ConfA and ConfC for ConfB
1473 // ...
1474 if (destURI != hostUri)
1475 newInfo.insert(newInfo.end(), confInfo.begin(), confInfo.end());
1476 }
1477 return newInfo;
1478}
1479
1480bool
1481Conference::isHost(std::string_view uri) const
1482{
1483 if (uri.empty())
1484 return true;
1485
1486 // Check if the URI is a local URI (AccountID) for at least one of the subcall
1487 // (a local URI can be in the call with another device)
1488 for (const auto& p : getSubCalls()) {
1489 if (auto call = getCall(p)) {
1490 if (auto account = call->getAccount().lock()) {
1491 if (account->getUsername() == uri)
1492 return true;
1493 }
1494 }
1495 }
1496 return false;
1497}
1498
1499bool
1500Conference::isHostDevice(std::string_view deviceId) const
1501{
1502 if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account_.lock()))
1503 return deviceId == acc->currentDeviceId();
1504 return false;
1505}
1506
1507void
1509{
1510 std::lock_guard lk(confInfoMutex_);
1511 confInfo_ = std::move(confInfo);
1512 sendConferenceInfos();
1513}
1514
1515void
1516Conference::hangupParticipant(const std::string& accountUri, const std::string& deviceId)
1517{
1518 if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account_.lock())) {
1519 if (deviceId.empty()) {
1520 // If deviceId is empty, hangup all calls with device
1521 while (auto call = getCallFromPeerID(accountUri)) {
1522 Manager::instance().hangupCall(acc->getAccountID(), call->getCallId());
1523 }
1524 return;
1525 } else {
1526 if (accountUri == acc->getUsername() && deviceId == acc->currentDeviceId()) {
1528 return;
1529 } else if (auto call = getCallWith(accountUri, deviceId)) {
1530 Manager::instance().hangupCall(acc->getAccountID(), call->getCallId());
1531 return;
1532 }
1533 }
1534 // Else, it may be a remote host
1535 auto remoteHost = findHostforRemoteParticipant(accountUri, deviceId);
1536 if (remoteHost.empty()) {
1537 JAMI_WARN("Unable to hangup %s, peer not found", accountUri.c_str());
1538 return;
1539 }
1540 if (auto call = getCallFromPeerID(string_remove_suffix(remoteHost, '@'))) {
1541 // Forward to the remote host.
1542 libjami::hangupParticipant(acc->getAccountID(), call->getCallId(), accountUri, deviceId);
1543 }
1544 }
1545}
1546
1547void
1548Conference::muteLocalHost(bool is_muted, const std::string& mediaType)
1549{
1550 if (mediaType.compare(libjami::Media::Details::MEDIA_TYPE_AUDIO) == 0) {
1552 JAMI_DEBUG("Local audio source already in [{:s}] state",
1553 is_muted ? "muted" : "un-muted");
1554 return;
1555 }
1556
1557 auto isHostMuted = isMuted("host"sv);
1559 JAMI_DBG("Muting local audio source");
1560 unbindHostAudio();
1562 JAMI_DBG("Un-muting local audio source");
1563 bindHostAudio();
1564 }
1566 updateMuted();
1568 return;
1569 } else if (mediaType.compare(libjami::Media::Details::MEDIA_TYPE_VIDEO) == 0) {
1570#ifdef ENABLE_VIDEO
1571 if (not isVideoEnabled()) {
1572 JAMI_ERROR("Unable to stop camera, the camera is disabled!");
1573 return;
1574 }
1575
1577 JAMI_DEBUG("Local camera source already in [{:s}] state",
1578 is_muted ? "stopped" : "started");
1579 return;
1580 }
1582 if (is_muted) {
1583 if (auto mixer = videoMixer_) {
1584 JAMI_DBG("Stopping local camera sources");
1585 mixer->stopInputs();
1586 }
1587 } else {
1588 if (auto mixer = videoMixer_) {
1589 JAMI_DBG("Starting local camera sources");
1590 std::vector<std::string> videoInputs;
1591 for (const auto& source : hostSources_) {
1592 if (source.type_ == MediaType::MEDIA_VIDEO)
1593 videoInputs.emplace_back(source.sourceUri_);
1594 }
1595 mixer->switchInputs(videoInputs);
1596 }
1597 }
1599 return;
1600#endif
1601 }
1602}
1603
1604#ifdef ENABLE_VIDEO
1605void
1606Conference::resizeRemoteParticipants(ConfInfo& confInfo, std::string_view peerURI)
1607{
1609 int remoteFrameWidth = confInfo.w;
1610
1611 if (remoteFrameHeight == 0 or remoteFrameWidth == 0) {
1612 // get the size of the remote frame from receiveThread
1613 // if the one from confInfo is empty
1614 if (auto call = std::dynamic_pointer_cast<SIPCall>(
1616 for (auto const& videoRtp : call->getRtpSessionList(MediaType::MEDIA_VIDEO)) {
1617 auto recv = std::static_pointer_cast<video::VideoRtpSession>(videoRtp)
1618 ->getVideoReceive();
1619 remoteFrameHeight = recv->getHeight();
1620 remoteFrameWidth = recv->getWidth();
1621 // NOTE: this may be not the behavior we want, but this is only called
1622 // when we receive conferences information from a call, so the peer is
1623 // mixing the video and send only one stream, so we can break here
1624 break;
1625 }
1626 }
1627 }
1628
1629 if (remoteFrameHeight == 0 or remoteFrameWidth == 0) {
1630 JAMI_WARN("Remote frame size not found.");
1631 return;
1632 }
1633
1634 // get the size of the local frame
1635 ParticipantInfo localCell;
1636 for (const auto& p : confInfo_) {
1637 if (p.uri == peerURI) {
1638 localCell = p;
1639 break;
1640 }
1641 }
1642
1643 const float zoomX = (float) remoteFrameWidth / localCell.w;
1644 const float zoomY = (float) remoteFrameHeight / localCell.h;
1645 // Do the resize for each remote participant
1646 for (auto& remoteCell : confInfo) {
1649 remoteCell.w = remoteCell.w / zoomX;
1650 remoteCell.h = remoteCell.h / zoomY;
1651 }
1652}
1653#endif
1654
1655void
1657{
1658 if (newInfo.empty()) {
1659 JAMI_DBG("confInfo empty, remove remoteHost");
1660 std::lock_guard lk(confInfoMutex_);
1661 remoteHosts_.erase(peerURI);
1662 sendConferenceInfos();
1663 return;
1664 }
1665
1666#ifdef ENABLE_VIDEO
1668#endif
1669
1670 bool updateNeeded = false;
1671 auto it = remoteHosts_.find(peerURI);
1672 if (it != remoteHosts_.end()) {
1673 // Compare confInfo before update
1674 if (it->second != newInfo) {
1675 it->second = newInfo;
1676 updateNeeded = true;
1677 } else
1678 JAMI_WARN("No change in confInfo, don't update");
1679 } else {
1680 remoteHosts_.emplace(peerURI, newInfo);
1681 updateNeeded = true;
1682 }
1683 // Send confInfo only if needed to avoid loops
1684#ifdef ENABLE_VIDEO
1685 if (updateNeeded and videoMixer_) {
1686 // Trigger the layout update in the mixer because the frame resolution may
1687 // change from participant to conference and cause a mismatch between
1688 // confInfo layout and rendering layout.
1689 videoMixer_->updateLayout();
1690 }
1691#endif
1692}
1693
1694std::string_view
1695Conference::findHostforRemoteParticipant(std::string_view uri, std::string_view deviceId)
1696{
1697 for (const auto& host : remoteHosts_) {
1698 for (const auto& p : host.second) {
1699 if (uri == string_remove_suffix(p.uri, '@') && (deviceId == "" || deviceId == p.device))
1700 return host.first;
1701 }
1702 }
1703 return "";
1704}
1705
1706std::shared_ptr<Call>
1708{
1709 for (const auto& p : getSubCalls()) {
1710 auto call = getCall(p);
1711 if (call && getRemoteId(call) == peerID) {
1712 return call;
1713 }
1714 }
1715 return nullptr;
1716}
1717
1718std::shared_ptr<Call>
1719Conference::getCallWith(const std::string& accountUri, const std::string& deviceId)
1720{
1721 for (const auto& p : getSubCalls()) {
1722 if (auto call = std::dynamic_pointer_cast<SIPCall>(getCall(p))) {
1723 auto* transport = call->getTransport();
1724 if (accountUri == string_remove_suffix(call->getPeerNumber(), '@') && transport
1725 && deviceId == transport->deviceId()) {
1726 return call;
1727 }
1728 }
1729 }
1730 return {};
1731}
1732
1733std::string
1734Conference::getRemoteId(const std::shared_ptr<jami::Call>& call) const
1735{
1736 if (auto* transport = std::dynamic_pointer_cast<SIPCall>(call)->getTransport())
1737 if (auto cert = transport->getTlsInfos().peerCert)
1738 if (cert->issuer)
1739 return cert->issuer->getId().toString();
1740 return {};
1741}
1742
1743void
1749
1750bool
1751Conference::startRecording(const std::string& path)
1752{
1753 auto res = Recordable::startRecording(path);
1755 return res;
1756}
1757
1759
1760void
1761Conference::bindHostAudio()
1762{
1763 JAMI_LOG("Bind host to conference {}", id_);
1764
1766
1767 for (const auto& item : getSubCalls()) {
1768 if (auto call = getCall(item)) {
1769 auto medias = call->getAudioStreams();
1770 for (const auto& [id, muted] : medias) {
1771 for (const auto& source : hostSources_) {
1772 if (source.type_ == MediaType::MEDIA_AUDIO) {
1773 // Start audio input
1774 auto hostAudioInput = hostAudioInputs_.find(source.label_);
1775 if (hostAudioInput == hostAudioInputs_.end()) {
1776 hostAudioInput = hostAudioInputs_
1777 .emplace(source.label_,
1778 std::make_shared<AudioInput>(source.label_))
1779 .first;
1780 }
1781 if (hostAudioInput != hostAudioInputs_.end()) {
1782 hostAudioInput->second->switchInput(source.sourceUri_);
1783 }
1784 // Bind audio
1785 if (source.label_ == sip_utils::DEFAULT_AUDIO_STREAMID) {
1786 bool isParticipantMuted = isMuted(call->getCallId());
1788 rbPool.bindHalfDuplexOut(id, RingBufferPool::DEFAULT_ID);
1789 else
1790 rbPool.bindRingBuffers(id, RingBufferPool::DEFAULT_ID);
1791 } else {
1792 auto buffer = source.sourceUri_;
1793 static const std::string& sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
1794 const auto pos = source.sourceUri_.find(sep);
1795 if (pos != std::string::npos)
1796 buffer = source.sourceUri_.substr(pos + sep.size());
1797
1798 rbPool.bindRingBuffers(id, buffer);
1799 }
1800 }
1801 }
1802 rbPool.flush(id);
1803 }
1804 }
1805 }
1807}
1808
1809void
1810Conference::unbindHostAudio()
1811{
1812 JAMI_LOG("Unbind host from conference {}", id_);
1813 for (const auto& source : hostSources_) {
1814 if (source.type_ == MediaType::MEDIA_AUDIO) {
1815 // Stop audio input
1816 auto hostAudioInput = hostAudioInputs_.find(source.label_);
1817 if (hostAudioInput != hostAudioInputs_.end()) {
1818 hostAudioInput->second->switchInput("");
1819 }
1820 // Unbind audio
1821 if (source.label_ == sip_utils::DEFAULT_AUDIO_STREAMID) {
1823 } else {
1824 auto buffer = source.sourceUri_;
1825 static const std::string& sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
1826 const auto pos = source.sourceUri_.find(sep);
1827 if (pos != std::string::npos)
1828 buffer = source.sourceUri_.substr(pos + sep.size());
1829
1831 }
1832 }
1833 }
1834}
1835
1836void
1837Conference::bindSubCallAudio(const std::string& callId)
1838{
1839 JAMI_LOG("Bind participant {} to conference {}", callId, id_);
1841
1842 // Bind each of the new participant's audio streams to each of the other participants audio streams
1843 if (auto participantCall = getCall(callId)) {
1844 auto participantStreams = participantCall->getAudioStreams();
1845 for (auto stream : participantStreams) {
1846 for (const auto& other : getSubCalls()) {
1847 auto otherCall = other != callId ? getCall(other) : nullptr;
1848 if (otherCall) {
1849 auto otherStreams = otherCall->getAudioStreams();
1850 for (auto otherStream : otherStreams) {
1851 if (isMuted(other))
1852 rbPool.bindHalfDuplexOut(otherStream.first, stream.first);
1853 else
1854 rbPool.bindRingBuffers(stream.first, otherStream.first);
1855
1856 rbPool.flush(otherStream.first);
1857 }
1858 }
1859 }
1860
1861 // Bind local participant to other participants only if the
1862 // local is attached to the conference.
1864 bool isHostMuted = isMuted("host"sv);
1866 rbPool.bindHalfDuplexOut(RingBufferPool::DEFAULT_ID, stream.first);
1867 else rbPool.bindRingBuffers(stream.first, RingBufferPool::DEFAULT_ID);
1869 }
1870 }
1871 }
1872}
1873
1874void
1875Conference::unbindSubCallAudio(const std::string& callId)
1876{
1877 JAMI_LOG("Unbind participant {} from conference {}", callId, id_);
1878 if (auto call = getCall(callId)) {
1879 auto medias = call->getAudioStreams();
1881 for (const auto& [id, muted] : medias) {
1883 }
1884 }
1885}
1886
1887
1888} // namespace jami
Main sound class.
std::shared_ptr< Call > getCall(const std::string &id) const
Return call pointer associated to given ID.Type can optionally be specified.
void onHangupParticipant(std::function< void(const std::string &, const std::string &)> &&cb)
void onMuteStreamAudio(std::function< void(const std::string &, const std::string &, const std::string &, bool)> &&cb)
void onKickParticipant(std::function< void(const std::string &)> &&cb)
void onRaiseHand(std::function< void(const std::string &, bool)> &&cb)
void initData(Json::Value &&d, std::string_view peerId)
Inject in the parser the data to parse.
void onMuteParticipant(std::function< void(const std::string &, bool)> &&cb)
void onSetActiveParticipant(std::function< void(const std::string &)> &&cb)
void onSetActiveStream(std::function< void(const std::string &, bool)> &&cb)
void onCheckAuthorization(std::function< bool(std::string_view)> &&cb)
Ask the caller to check if a peer is authorized (moderator of the conference)
void onSetLayout(std::function< void(int)> &&cb)
void onVersion(std::function< void(uint32_t)> &&cb)
void parse()
Parse the datas, this will call the methods injected if necessary.
void onVoiceActivity(std::function< void(const std::string &, bool)> &&cb)
void onRaiseHandUri(std::function< void(const std::string &, bool)> &&cb)
const char * getStateStr() const
Definition conference.h:245
void attachHost(const std::vector< libjami::MediaMap > &mediaList={})
Attach host.
void setVoiceActivity(const std::string &streamId, const bool &newState)
std::string getAccountId() const
bool isVideoEnabled() const
void hangupParticipant(const std::string &accountUri, const std::string &deviceId="")
void muteLocalHost(bool is_muted, const std::string &mediaType)
std::chrono::milliseconds getDuration() const
Definition conference.h:375
bool startRecording(const std::string &path) override
Start recording.
const std::string & getConfId() const
Return the conference id.
Definition conference.h:207
void removeSubCall(const std::string &callId)
Remove a subcall from the conference.
std::vector< libjami::MediaMap > currentMediaList() const
Retrieve current medias list.
void detachHost()
Detach local audio/video from the conference.
~Conference()
Destructor for this class, decrement static counter.
void mergeConfInfo(ConfInfo &newInfo, const std::string &peerURI)
void setActiveParticipant(const std::string &participant_id)
void handleMediaChangeRequest(const std::shared_ptr< Call > &call, const std::vector< libjami::MediaMap > &remoteMediaList)
Process incoming media change request.
void setLocalHostMuteState(MediaType type, bool muted)
Set the mute state of the local host.
void setState(State state)
Set conference state.
bool requestMediaChange(const std::vector< libjami::MediaMap > &mediaList)
Process a media change request.
void onConfOrder(const std::string &callId, const std::string &order)
void addSubCall(const std::string &callId)
Add a new subcall to the conference.
std::shared_ptr< Account > getAccount() const
Definition conference.h:209
Conference(const std::shared_ptr< Account > &, const std::string &confId="")
Constructor for this class, increment static counter.
std::shared_ptr< Call > getCallFromPeerID(std::string_view peerId)
void setActiveStream(const std::string &streamId, bool state)
void setModerator(const std::string &uri, const bool &state)
void stopRecording() override
Stop recording.
bool toggleRecording() override
Start/stop recording toggle.
CallIdSet getSubCalls() const
Get the participant list for this conference.
void setHandRaised(const std::string &uri, const bool &state)
void muteParticipant(const std::string &uri, const bool &state)
State getState() const
Return the current conference state.
void muteStream(const std::string &accountUri, const std::string &deviceId, const std::string &streamId, const bool &state)
The client shows one tile per stream (video/audio related to a media)
void updateConferenceInfo(ConfInfo confInfo)
void switchInput(const std::string &input)
void setLayout(int layout)
void reportMediaNegotiationStatus()
Announce to the client that medias are successfully negotiated.
bool isMediaSourceMuted(MediaType type) const
Get the mute state of the local host.
Ring Account is build on top of SIPAccountBase and uses DHT to handle call connectivity.
Definition jamiaccount.h:96
Manager (controller) of daemon.
Definition manager.h:67
std::vector< std::shared_ptr< T > > getAllAccounts() const
Get a list of account pointers of type T (baseclass Account)
Definition manager.h:731
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:676
CallFactory callFactory
Definition manager.h:794
VideoManager * getVideoManager() const
Definition manager.cpp:3175
bool hangupCall(const std::string &accountId, const std::string &callId)
Functions which occur with a user's action Hangup the call.
Definition manager.cpp:1142
bool detachHost(const std::shared_ptr< Conference > &conf={})
Detach the local participant from curent conference.
Definition manager.cpp:1558
RingBufferPool & getRingBufferPool()
Return a pointer to the instance of the RingBufferPool.
Definition manager.cpp:3157
static std::vector< MediaAttribute > buildMediaAttributesList(const std::vector< libjami::MediaMap > &mediaList, bool secure)
static bool hasMediaType(const std::vector< MediaAttribute > &mediaList, MediaType type)
static char const * mediaTypeToString(MediaType type)
static libjami::MediaMap toMediaMap(const MediaAttribute &mediaAttr)
static std::vector< libjami::MediaMap > mediaAttributesToMediaMaps(std::vector< MediaAttribute > mediaAttrList)
virtual bool startRecording(const std::string &path)
Start recording.
virtual void stopRecording()
Stop recording.
virtual bool toggleRecording()
This method must be implemented for this interface as calls and conferences have different behavior.
std::shared_ptr< MediaRecorder > recorder_
Definition recordable.h:69
bool isRecording() const
Return recording state (true/false)
Definition recordable.h:36
void unBindAllHalfDuplexIn(const std::string &sourceBufferId)
Detaches a source from all its readers.
static const char *const DEFAULT_ID
void unBindAll(const std::string &ringbufferId)
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_ERROR(formatstr,...)
Definition logger.h:228
#define JAMI_DBG(...)
Definition logger.h:216
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARN(...)
Definition logger.h:217
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
#define JAMI_LOG(formatstr,...)
Definition logger.h:225
#define JAMI_INFO(...)
Definition logger.h:215
std::string toString(const Json::Value &jsonVal)
Definition json_utils.h:42
bool parse(std::string_view jsonStr, Json::Value &jsonVal)
Definition json_utils.h:29
std::string streamId(const std::string &callId, std::string_view label)
constexpr std::string_view DEFAULT_VIDEO_STREAMID
Definition sip_utils.h:146
constexpr std::string_view DEFAULT_AUDIO_STREAMID
Definition sip_utils.h:147
std::set< std::string > CallIdSet
Definition conference.h:185
static constexpr const char TRUE_STR[]
bool closeMediaPlayer(const std::string &id)
void emitSignal(Args... args)
Definition ring_signal.h:64
std::shared_ptr< AudioInput > getAudioInput(const std::string &device)
std::vector< unsigned > split_string_to_unsigned(std::string_view str, char delim)
std::string_view string_remove_suffix(std::string_view str, char separator)
static constexpr const char FALSE_STR[]
@ MEDIA_AUDIO
Definition media_codec.h:47
@ MEDIA_VIDEO
Definition media_codec.h:48
@ MEDIA_NONE
Definition media_codec.h:46
std::string createMediaPlayer(const std::string &path)
static void runOnMainThread(Callback &&cb)
Definition manager.h:909
static constexpr char MEDIA_TYPE_AUDIO[]
Definition media_const.h:38
static constexpr char MEDIA_TYPE_VIDEO[]
Definition media_const.h:39
static constexpr char ENABLED[]
Definition media_const.h:51
static constexpr char MUTED[]
Definition media_const.h:52
static constexpr const char * SEPARATOR
Definition media_const.h:33
static constexpr const char * FILE
Definition media_const.h:31
void hangupParticipant(const std::string &accountId, const std::string &confId, const std::string &accountUri, const std::string &deviceId)
SIPCall are SIP implementation of a normal Call.
Contains information about an AV subject.
Definition streamdata.h:29
const bool direction
Definition streamdata.h:51
const std::string id
Definition streamdata.h:49
const StreamType type
Definition streamdata.h:53
std::string toString() const
std::vector< std::map< std::string, std::string > > toVectorMapStringString() const
#define jami_tracepoint(...)
Definition tracepoint.h:53