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