LCOV - code coverage report
Current view: top level - foo/src - conference.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 744 1079 69.0 %
Date: 2025-10-16 08:11:43 Functions: 116 163 71.2 %

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

Generated by: LCOV version 1.14