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