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