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