LCOV - code coverage report
Current view: top level - src - conference.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 65.9 % 1222 805
Test Date: 2026-06-13 09:18:46 Functions: 57.2 % 229 131

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

Generated by: LCOV version 2.0-1