LCOV - code coverage report
Current view: top level - src - conference.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 65.6 % 1228 805
Test Date: 2026-11-09 10:41:14 Functions: 76.2 % 101 77

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

Generated by: LCOV version 2.0-1