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