LCOV - code coverage report
Current view: top level - foo/src - conference.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 805 1222 65.9 %
Date: 2026-04-01 09:29:43 Functions: 130 229 56.8 %

          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

Generated by: LCOV version 1.14