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