LCOV - code coverage report
Current view: top level - foo/src - conference.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 739 1083 68.2 %
Date: 2025-12-18 10:07:43 Functions: 115 196 58.7 %

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

Generated by: LCOV version 1.14