| /* |
| * Copyright (C) 2004-2021 Savoir-faire Linux Inc. |
| * |
| * Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com> |
| * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include <regex> |
| #include <sstream> |
| |
| #include "conference.h" |
| #include "manager.h" |
| #include "audio/audiolayer.h" |
| #include "jamidht/jamiaccount.h" |
| #include "string_utils.h" |
| |
| #ifdef ENABLE_VIDEO |
| #include "call.h" |
| #include "client/videomanager.h" |
| #include "video/video_input.h" |
| #include "video/video_mixer.h" |
| #endif |
| |
| #ifdef ENABLE_PLUGIN |
| #include "plugin/jamipluginmanager.h" |
| #endif |
| |
| #include "call_factory.h" |
| |
| #include "logger.h" |
| #include "jami/media_const.h" |
| #include "audio/ringbufferpool.h" |
| #include "sip/sipcall.h" |
| |
| #include <opendht/thread_pool.h> |
| |
| using namespace std::literals; |
| |
| namespace jami { |
| |
| Conference::Conference(const std::shared_ptr<Account>& account) |
| : id_(Manager::instance().callFactory.getNewCallID()) |
| , account_(account) |
| #ifdef ENABLE_VIDEO |
| , videoEnabled_(account->isVideoEnabled()) |
| #endif |
| { |
| /** NOTE: |
| * |
| *** Handling mute state of the local host. |
| * |
| * When a call is added to a conference, the media source of the |
| * call is set to the audio/video mixers output, and the host media |
| * source (e.g. camera), is added as a source for the mixer. |
| * Note that, by design, the mixers are never muted, but the mixer |
| * can produce audio/video frames with no content (silence or black |
| * video frames) if all the participants are muted. |
| * |
| * The mute state of the local host is set as follows: |
| * |
| * 1. If the video is disabled, the mute state is irrelevant. |
| * 2. If the local is not attached, the mute state is irrelevant. |
| * 3. When the conference is created from existing calls: |
| * the mute state is set to true if the local mute state of |
| * all participating calls are true. |
| * 4. Attaching the local host to an existing conference: |
| * the audio and video is set to the default capture device |
| * (microphone and/or camera), and set to un-muted state. |
| */ |
| |
| JAMI_INFO("Create new conference %s", id_.c_str()); |
| setLocalHostDefaultMediaSource(); |
| |
| #ifdef ENABLE_VIDEO |
| // We are done if the video is disabled. |
| if (not videoEnabled_) |
| return; |
| |
| videoMixer_ = std::make_shared<video::VideoMixer>(id_, hostVideoSource_.sourceUri_); |
| videoMixer_->setOnSourcesUpdated([this](std::vector<video::SourceInfo>&& infos) { |
| runOnMainThread([w = weak(), infos = std::move(infos)] { |
| auto shared = w.lock(); |
| if (!shared) |
| return; |
| ConfInfo newInfo; |
| auto hostAdded = false; |
| // Handle participants showing their video |
| std::unique_lock<std::mutex> lk(shared->videoToCallMtx_); |
| for (const auto& info : infos) { |
| std::string uri {}; |
| auto it = shared->videoToCall_.find(info.source); |
| if (it == shared->videoToCall_.end()) |
| it = shared->videoToCall_.emplace_hint(it, info.source, std::string()); |
| bool isLocalMuted = false; |
| // If not local |
| if (!it->second.empty()) { |
| // Retrieve calls participants |
| // TODO: this is a first version, we assume that the peer is not |
| // a master of a conference and there is only one remote |
| // In the future, we should retrieve confInfo from the call |
| // To merge layouts informations |
| if (auto call = getCall(it->second)) { |
| uri = call->getPeerNumber(); |
| isLocalMuted = call->isPeerMuted(); |
| } |
| } |
| auto active = false; |
| if (auto videoMixer = shared->videoMixer_) |
| active = info.source == videoMixer->getActiveParticipant(); |
| std::string_view peerId = string_remove_suffix(uri, '@'); |
| auto isModerator = shared->isModerator(peerId); |
| if (uri.empty()) { |
| hostAdded = true; |
| peerId = "host"sv; |
| isLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); |
| } |
| auto isHandRaised = shared->isHandRaised(peerId); |
| auto isModeratorMuted = shared->isMuted(peerId); |
| auto sinkId = shared->getConfId() + peerId; |
| newInfo.emplace_back(ParticipantInfo {std::move(uri), |
| {}, |
| std::move(sinkId), |
| active, |
| info.x, |
| info.y, |
| info.w, |
| info.h, |
| !info.hasVideo, |
| isLocalMuted, |
| isModeratorMuted, |
| isModerator, |
| isHandRaised}); |
| } |
| if (auto videoMixer = shared->videoMixer_) { |
| newInfo.h = videoMixer->getHeight(); |
| newInfo.w = videoMixer->getWidth(); |
| } |
| lk.unlock(); |
| if (!hostAdded) { |
| ParticipantInfo pi; |
| pi.videoMuted = true; |
| pi.audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); |
| pi.isModerator = true; |
| newInfo.emplace_back(pi); |
| } |
| |
| shared->updateConferenceInfo(std::move(newInfo)); |
| }); |
| }); |
| #endif |
| } |
| |
| Conference::~Conference() |
| { |
| JAMI_INFO("Destroying conference %s", id_.c_str()); |
| |
| #ifdef ENABLE_VIDEO |
| foreachCall([&](auto call) { |
| call->exitConference(); |
| // Reset distant callInfo |
| call->resetConfInfo(); |
| // Trigger the SIP negotiation to update the resolution for the remaining call |
| // ideally this sould be done without renegotiation |
| call->switchInput( |
| Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice()); |
| |
| // Continue the recording for the call if the conference was recorded |
| if (isRecording()) { |
| JAMI_DBG("Stop recording for conf %s", getConfId().c_str()); |
| toggleRecording(); |
| if (not call->isRecording()) { |
| JAMI_DBG("Conference was recorded, start recording for conf %s", |
| call->getCallId().c_str()); |
| call->toggleRecording(); |
| } |
| } |
| // Notify that the remaining peer is still recording after conference |
| if (call->isPeerRecording()) |
| call->peerRecording(true); |
| }); |
| for (auto it = confSinksMap_.begin(); it != confSinksMap_.end();) { |
| if (videoMixer_) |
| videoMixer_->detach(it->second.get()); |
| it->second->stop(); |
| it = confSinksMap_.erase(it); |
| } |
| #endif // ENABLE_VIDEO |
| #ifdef ENABLE_PLUGIN |
| { |
| std::lock_guard<std::mutex> lk(avStreamsMtx_); |
| jami::Manager::instance() |
| .getJamiPluginManager() |
| .getCallServicesManager() |
| .clearCallHandlerMaps(getConfId()); |
| Manager::instance().getJamiPluginManager().getCallServicesManager().clearAVSubject( |
| getConfId()); |
| confAVStreams.clear(); |
| } |
| #endif // ENABLE_PLUGIN |
| } |
| |
| Conference::State |
| Conference::getState() const |
| { |
| return confState_; |
| } |
| |
| void |
| Conference::setState(State state) |
| { |
| JAMI_DBG("[conf %s] Set state to [%s] (was [%s])", |
| id_.c_str(), |
| getStateStr(state), |
| getStateStr()); |
| |
| confState_ = state; |
| } |
| |
| void |
| Conference::setLocalHostDefaultMediaSource() |
| { |
| // Setup local audio source |
| if (confState_ == State::ACTIVE_ATTACHED) { |
| hostAudioSource_ = {MediaType::MEDIA_AUDIO, false, false, true, {}, "audio_0"}; |
| hostAudioSource_.sourceType_ = MediaSourceType::CAPTURE_DEVICE; |
| } else { |
| hostAudioSource_ = {}; |
| } |
| |
| JAMI_DBG("[conf %s] Setting local host audio source to [%s]", |
| id_.c_str(), |
| hostAudioSource_.toString().c_str()); |
| |
| #ifdef ENABLE_VIDEO |
| if (isVideoEnabled()) { |
| // Setup local video source |
| if (confState_ == State::ACTIVE_ATTACHED) { |
| hostVideoSource_ |
| = {MediaType::MEDIA_VIDEO, |
| false, |
| false, |
| true, |
| Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice(), |
| "video_0"}; |
| hostVideoSource_.sourceType_ = MediaSourceType::CAPTURE_DEVICE; |
| } else { |
| hostVideoSource_ = {}; |
| } |
| JAMI_DBG("[conf %s] Setting local host video source to [%s]", |
| id_.c_str(), |
| hostVideoSource_.toString().c_str()); |
| } |
| #endif |
| } |
| |
| #ifdef ENABLE_PLUGIN |
| void |
| Conference::createConfAVStreams() |
| { |
| auto audioMap = [](const std::shared_ptr<jami::MediaFrame>& m) -> AVFrame* { |
| return std::static_pointer_cast<AudioFrame>(m)->pointer(); |
| }; |
| |
| // Preview and Received |
| if ((audioMixer_ = jami::getAudioInput(getConfId()))) { |
| auto audioSubject = std::make_shared<MediaStreamSubject>(audioMap); |
| StreamData previewStreamData {getConfId(), false, StreamType::audio, getConfId()}; |
| createConfAVStream(previewStreamData, *audioMixer_, audioSubject); |
| StreamData receivedStreamData {getConfId(), true, StreamType::audio, getConfId()}; |
| createConfAVStream(receivedStreamData, *audioMixer_, audioSubject); |
| } |
| |
| #ifdef ENABLE_VIDEO |
| |
| if (videoMixer_) { |
| // Review |
| auto receiveSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_); |
| StreamData receiveStreamData {getConfId(), true, StreamType::video, getConfId()}; |
| createConfAVStream(receiveStreamData, *videoMixer_, receiveSubject); |
| |
| // Preview |
| if (auto& videoPreview = videoMixer_->getVideoLocal()) { |
| auto previewSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_); |
| StreamData previewStreamData {getConfId(), false, StreamType::video, getConfId()}; |
| createConfAVStream(previewStreamData, *videoPreview, previewSubject); |
| } |
| } |
| #endif // ENABLE_VIDEO |
| } |
| |
| void |
| Conference::createConfAVStream(const StreamData& StreamData, |
| AVMediaStream& streamSource, |
| const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject, |
| bool force) |
| { |
| std::lock_guard<std::mutex> lk(avStreamsMtx_); |
| const std::string AVStreamId = StreamData.id + std::to_string(static_cast<int>(StreamData.type)) |
| + std::to_string(StreamData.direction); |
| auto it = confAVStreams.find(AVStreamId); |
| if (!force && it != confAVStreams.end()) |
| return; |
| |
| confAVStreams.erase(AVStreamId); |
| confAVStreams[AVStreamId] = mediaStreamSubject; |
| streamSource.attachPriorityObserver(mediaStreamSubject); |
| jami::Manager::instance() |
| .getJamiPluginManager() |
| .getCallServicesManager() |
| .createAVSubject(StreamData, mediaStreamSubject); |
| } |
| #endif // ENABLE_PLUGIN |
| |
| void |
| Conference::setLocalHostMuteState(MediaType type, bool muted) |
| { |
| if (type == MediaType::MEDIA_AUDIO) { |
| hostAudioSource_.muted_ = muted; |
| } else if (type == MediaType::MEDIA_VIDEO) { |
| hostVideoSource_.muted_ = muted; |
| } else { |
| JAMI_ERR("Unsupported media type"); |
| } |
| } |
| |
| bool |
| Conference::isMediaSourceMuted(MediaType type) const |
| { |
| if (getState() != State::ACTIVE_ATTACHED) { |
| // Assume muted if not attached. |
| return true; |
| } |
| |
| if (type != MediaType::MEDIA_AUDIO and type != MediaType::MEDIA_VIDEO) { |
| JAMI_ERR("Unsupported media type"); |
| return true; |
| } |
| |
| auto const& mediaAttr = type == MediaType::MEDIA_AUDIO ? hostAudioSource_ : hostVideoSource_; |
| if (mediaAttr.type_ == MediaType::MEDIA_NONE) { |
| JAMI_WARN("The host source for %s is not set. The mute state is meaningless", |
| mediaAttr.mediaTypeToString(mediaAttr.type_)); |
| // Assume muted if the media is not present. |
| return true; |
| } |
| |
| return mediaAttr.muted_; |
| } |
| |
| void |
| Conference::takeOverMediaSourceControl(const std::string& callId) |
| { |
| auto call = getCall(callId); |
| if (not call) { |
| JAMI_ERR("No call matches participant %s", callId.c_str()); |
| return; |
| } |
| |
| auto account = call->getAccount().lock(); |
| if (not account) { |
| JAMI_ERR("No account detected for call %s", callId.c_str()); |
| return; |
| } |
| |
| auto mediaList = call->getMediaAttributeList(); |
| |
| std::vector<MediaType> mediaTypeList {MediaType::MEDIA_AUDIO, MediaType::MEDIA_VIDEO}; |
| |
| for (auto mediaType : mediaTypeList) { |
| // Try to find a media with a valid source type |
| auto check = [mediaType](auto const& mediaAttr) { |
| return (mediaAttr.type_ == mediaType and mediaAttr.sourceType_ != MediaSourceType::NONE); |
| }; |
| |
| auto iter = std::find_if(mediaList.begin(), mediaList.end(), check); |
| |
| if (iter == mediaList.end()) { |
| // Nothing to do if the call does not have a stream with |
| // the requested media. |
| JAMI_DBG("[Call: %s] Does not have an active [%s] media source", |
| callId.c_str(), |
| MediaAttribute::mediaTypeToString(mediaType)); |
| continue; |
| } |
| |
| if (getState() == State::ACTIVE_ATTACHED) { |
| // To mute the local source, all the sources of the participating |
| // calls must be muted. If it's the first participant, just use |
| // its mute state. |
| if (participants_.size() == 1) { |
| setLocalHostMuteState(iter->type_, iter->muted_); |
| } else { |
| setLocalHostMuteState(iter->type_, iter->muted_ and isMediaSourceMuted(iter->type_)); |
| } |
| } |
| |
| // Un-mute media in the call. The mute/un-mute state will be handled |
| // by the conference/mixer from now on. |
| iter->muted_ = false; |
| } |
| |
| // Update the media states in the newly added call. |
| call->requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList)); |
| |
| // Notify the client |
| for (auto mediaType : mediaTypeList) { |
| if (mediaType == MediaType::MEDIA_AUDIO) { |
| bool muted = isMediaSourceMuted(MediaType::MEDIA_AUDIO); |
| JAMI_WARN("Take over [AUDIO] control from call %s - current local source state [%s]", |
| callId.c_str(), |
| muted ? "muted" : "un-muted"); |
| emitSignal<DRing::CallSignal::AudioMuted>(id_, muted); |
| } else { |
| bool muted = isMediaSourceMuted(MediaType::MEDIA_VIDEO); |
| JAMI_WARN("Take over [VIDEO] control from call %s - current local source state [%s]", |
| callId.c_str(), |
| muted ? "muted" : "un-muted"); |
| emitSignal<DRing::CallSignal::VideoMuted>(id_, muted); |
| } |
| } |
| } |
| |
| bool |
| Conference::requestMediaChange(const std::vector<DRing::MediaMap>& mediaList) |
| { |
| if (getState() != State::ACTIVE_ATTACHED) { |
| JAMI_ERR("[conf %s] Request media change can be performed only in attached mode", |
| getConfId().c_str()); |
| return false; |
| } |
| |
| JAMI_DBG("[conf %s] Request media change", getConfId().c_str()); |
| |
| auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, false); |
| |
| for (auto const& mediaAttr : mediaAttrList) { |
| JAMI_DBG("[conf %s] New requested media: %s", |
| getConfId().c_str(), |
| mediaAttr.toString(true).c_str()); |
| } |
| |
| // NOTE: |
| // The current design support only one stream per media type. The |
| // request will be ignored if this condition is not respected. |
| for (auto mediaType : {MediaType::MEDIA_AUDIO, MediaType::MEDIA_VIDEO}) { |
| auto count = std::count_if(mediaAttrList.begin(), |
| mediaAttrList.end(), |
| [&mediaType](auto const& attr) { |
| return attr.type_ == mediaType; |
| }); |
| |
| if (count > 1) { |
| JAMI_ERR("[conf %s] Cant handle more than 1 stream per media type (found %lu)", |
| getConfId().c_str(), |
| count); |
| return false; |
| } |
| } |
| |
| for (auto const& mediaAttr : mediaAttrList) { |
| auto& mediaSource = mediaAttr.type_ == MediaType::MEDIA_AUDIO ? hostAudioSource_ |
| : hostVideoSource_; |
| |
| if (not mediaAttr.sourceUri_.empty() and mediaSource.sourceUri_ != mediaAttr.sourceUri_) { |
| // For now, only video source URI can be changed by the client, |
| // so it's an error if we get here and the type is not video. |
| if (mediaAttr.type_ != MediaType::MEDIA_VIDEO) { |
| JAMI_ERR("[conf %s] Media source can be changed only for video!", |
| getConfId().c_str()); |
| return false; |
| } |
| |
| mediaSource.sourceUri_ = mediaAttr.sourceUri_; |
| mediaSource.sourceType_ = mediaAttr.sourceType_; |
| |
| if (mediaSource.muted_ != mediaAttr.muted_) { |
| // If the current media source is muted, just call un-mute, it |
| // will set the new source as input. |
| muteLocalHost(mediaAttr.muted_, |
| mediaAttr.type_ == MediaType::MEDIA_AUDIO |
| ? DRing::Media::Details::MEDIA_TYPE_AUDIO |
| : DRing::Media::Details::MEDIA_TYPE_VIDEO); |
| } else { |
| switchInput(mediaSource.sourceUri_); |
| } |
| } |
| |
| // Update the mute state if changed. |
| if (mediaSource.muted_ != mediaAttr.muted_) { |
| muteLocalHost(mediaAttr.muted_, |
| mediaAttr.type_ == MediaType::MEDIA_AUDIO |
| ? DRing::Media::Details::MEDIA_TYPE_AUDIO |
| : DRing::Media::Details::MEDIA_TYPE_VIDEO); |
| } |
| } |
| |
| return true; |
| } |
| |
| void |
| Conference::handleMediaChangeRequest(const std::shared_ptr<Call>& call, |
| const std::vector<DRing::MediaMap>& remoteMediaList) |
| { |
| JAMI_DBG("Conf [%s] Answer to media change request", getConfId().c_str()); |
| |
| // If the new media list has video, remove existing dummy |
| // video sessions if any. |
| if (MediaAttribute::hasMediaType(MediaAttribute::buildMediaAttributesList(remoteMediaList, |
| false), |
| MediaType::MEDIA_VIDEO)) { |
| call->removeDummyVideoRtpSessions(); |
| } |
| |
| // Check if we need to update the mixer. |
| // We need to check before the media is changed. |
| auto updateMixer = call->checkMediaChangeRequest(remoteMediaList); |
| |
| // NOTE: |
| // Since this is a conference, accept any media change request. |
| // This also means that if original call was an audio-only call, |
| // the local camera will be enabled, unless the video is disabled |
| // in the account settings. |
| |
| call->answerMediaChangeRequest(remoteMediaList); |
| call->enterConference(shared_from_this()); |
| |
| if (updateMixer and getState() == Conference::State::ACTIVE_ATTACHED) { |
| detachLocalParticipant(); |
| attachLocalParticipant(); |
| } |
| } |
| |
| void |
| Conference::addParticipant(const std::string& participant_id) |
| { |
| JAMI_DBG("Adding call %s to conference %s", participant_id.c_str(), id_.c_str()); |
| |
| { |
| std::lock_guard<std::mutex> lk(participantsMtx_); |
| if (!participants_.insert(participant_id).second) |
| return; |
| } |
| |
| // Check if participant was muted before conference |
| if (auto call = getCall(participant_id)) { |
| if (call->isPeerMuted()) { |
| participantsMuted_.emplace(string_remove_suffix(call->getPeerNumber(), '@')); |
| } |
| |
| // NOTE: |
| // When a call joins a conference, the media source of the call |
| // will be set to the output of the conference mixer. |
| takeOverMediaSourceControl(participant_id); |
| } |
| |
| if (auto call = getCall(participant_id)) { |
| auto w = call->getAccount(); |
| auto account = w.lock(); |
| if (account) { |
| // Add defined moderators for the account link to the call |
| for (const auto& mod : account->getDefaultModerators()) { |
| moderators_.emplace(mod); |
| } |
| |
| // Check for localModeratorsEnabled preference |
| if (account->isLocalModeratorsEnabled() && not localModAdded_) { |
| auto accounts = jami::Manager::instance().getAllAccounts<JamiAccount>(); |
| for (const auto& account : accounts) { |
| moderators_.emplace(account->getUsername()); |
| } |
| localModAdded_ = true; |
| } |
| |
| // Check for allModeratorEnabled preference |
| if (account->isAllModerators()) { |
| moderators_.emplace(string_remove_suffix(call->getPeerNumber(), '@')); |
| } |
| } |
| } |
| #ifdef ENABLE_VIDEO |
| if (auto call = getCall(participant_id)) { |
| // In conference, all participants need to have video session |
| // (with a sink) in order to display the participant info in |
| // the layout. So, if a participant joins with an audio only |
| // call, a dummy video stream is added to the call. |
| auto mediaList = call->getMediaAttributeList(); |
| if (not MediaAttribute::hasMediaType(mediaList, MediaType::MEDIA_VIDEO)) { |
| call->addDummyVideoRtpSession(); |
| } |
| call->enterConference(shared_from_this()); |
| // Continue the recording for the conference if one participant was recording |
| if (call->isRecording()) { |
| JAMI_DBG("Stop recording for call %s", call->getCallId().c_str()); |
| call->toggleRecording(); |
| if (not this->isRecording()) { |
| JAMI_DBG("One participant was recording, start recording for conference %s", |
| getConfId().c_str()); |
| this->toggleRecording(); |
| } |
| } |
| } else |
| JAMI_ERR("no call associate to participant %s", participant_id.c_str()); |
| #endif // ENABLE_VIDEO |
| #ifdef ENABLE_PLUGIN |
| createConfAVStreams(); |
| #endif |
| } |
| |
| void |
| Conference::setActiveParticipant(const std::string& participant_id) |
| { |
| if (!videoMixer_) |
| return; |
| if (isHost(participant_id)) { |
| videoMixer_->setActiveHost(); |
| return; |
| } |
| if (auto call = getCallFromPeerID(participant_id)) { |
| if (auto videoRecv = call->getReceiveVideoFrameActiveWriter()) |
| videoMixer_->setActiveParticipant(videoRecv.get()); |
| return; |
| } |
| |
| auto remoteHost = findHostforRemoteParticipant(participant_id); |
| if (not remoteHost.empty()) { |
| // This logic will be handled client side |
| JAMI_WARN("Change remote layout is not supported"); |
| return; |
| } |
| // Unset active participant by default |
| videoMixer_->setActiveParticipant(nullptr); |
| } |
| |
| void |
| Conference::setLayout(int layout) |
| { |
| switch (layout) { |
| case 0: |
| videoMixer_->setVideoLayout(video::Layout::GRID); |
| // The layout shouldn't have an active participant |
| if (videoMixer_->getActiveParticipant()) |
| videoMixer_->setActiveParticipant(nullptr); |
| break; |
| case 1: |
| videoMixer_->setVideoLayout(video::Layout::ONE_BIG_WITH_SMALL); |
| break; |
| case 2: |
| videoMixer_->setVideoLayout(video::Layout::ONE_BIG); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| std::vector<std::map<std::string, std::string>> |
| ConfInfo::toVectorMapStringString() const |
| { |
| std::vector<std::map<std::string, std::string>> infos; |
| infos.reserve(size()); |
| for (const auto& info : *this) |
| infos.emplace_back(info.toMap()); |
| return infos; |
| } |
| |
| std::string |
| ConfInfo::toString() const |
| { |
| Json::Value val = {}; |
| for (const auto& info : *this) { |
| val["p"].append(info.toJson()); |
| } |
| val["w"] = w; |
| val["h"] = h; |
| return Json::writeString(Json::StreamWriterBuilder {}, val); |
| } |
| |
| void |
| Conference::sendConferenceInfos() |
| { |
| // Inform calls that the layout has changed |
| foreachCall([&](auto call) { |
| // Produce specific JSON for each participant (2 separate accounts can host ... |
| // a conference on a same device, the conference is not link to one account). |
| auto w = call->getAccount(); |
| auto account = w.lock(); |
| if (!account) |
| return; |
| |
| dht::ThreadPool::io().run( |
| [call, |
| confInfo = getConfInfoHostUri(account->getUsername() + "@ring.dht", |
| call->getPeerNumber())] { |
| call->sendConfInfo(confInfo.toString()); |
| }); |
| }); |
| |
| auto confInfo = getConfInfoHostUri("", ""); |
| createSinks(confInfo); |
| |
| // Inform client that layout has changed |
| jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>(id_, |
| confInfo.toVectorMapStringString()); |
| } |
| |
| void |
| Conference::createSinks(const ConfInfo& infos) |
| { |
| #ifdef ENABLE_VIDEO |
| std::lock_guard<std::mutex> lk(sinksMtx_); |
| if (!videoMixer_) |
| return; |
| |
| Manager::instance().createSinkClients(getConfId(), |
| infos, |
| std::static_pointer_cast<video::VideoGenerator>( |
| videoMixer_), |
| confSinksMap_); |
| #endif |
| } |
| |
| void |
| Conference::attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame, const std::string& callId) |
| { |
| std::lock_guard<std::mutex> lk(videoToCallMtx_); |
| videoToCall_.emplace(frame, callId); |
| frame->attach(videoMixer_.get()); |
| } |
| |
| void |
| Conference::detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame) |
| { |
| std::lock_guard<std::mutex> lk(videoToCallMtx_); |
| auto it = videoToCall_.find(frame); |
| if (it != videoToCall_.end()) { |
| it->first->detach(videoMixer_.get()); |
| videoToCall_.erase(it); |
| } |
| } |
| |
| void |
| Conference::removeParticipant(const std::string& participant_id) |
| { |
| { |
| std::lock_guard<std::mutex> lk(participantsMtx_); |
| if (!participants_.erase(participant_id)) |
| return; |
| } |
| if (auto call = getCall(participant_id)) { |
| participantsMuted_.erase(std::string(string_remove_suffix(call->getPeerNumber(), '@'))); |
| handsRaised_.erase(std::string(string_remove_suffix(call->getPeerNumber(), '@'))); |
| #ifdef ENABLE_VIDEO |
| call->exitConference(); |
| if (call->isPeerRecording()) |
| call->peerRecording(false); |
| #endif // ENABLE_VIDEO |
| } |
| } |
| |
| void |
| Conference::attachLocalParticipant() |
| { |
| JAMI_INFO("Attach local participant to conference %s", id_.c_str()); |
| |
| if (getState() == State::ACTIVE_DETACHED) { |
| setState(State::ACTIVE_ATTACHED); |
| setLocalHostDefaultMediaSource(); |
| |
| auto& rbPool = Manager::instance().getRingBufferPool(); |
| for (const auto& participant : getParticipantList()) { |
| if (auto call = Manager::instance().getCallFromCallID(participant)) { |
| if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) |
| rbPool.bindHalfDuplexOut(participant, RingBufferPool::DEFAULT_ID); |
| else |
| rbPool.bindCallID(participant, RingBufferPool::DEFAULT_ID); |
| rbPool.flush(participant); |
| } |
| |
| // Reset ringbuffer's readpointers |
| rbPool.flush(participant); |
| } |
| rbPool.flush(RingBufferPool::DEFAULT_ID); |
| |
| #ifdef ENABLE_VIDEO |
| if (videoMixer_) { |
| videoMixer_->switchInput(hostVideoSource_.sourceUri_); |
| if (not mediaSecondaryInput_.empty()) |
| videoMixer_->switchSecondaryInput(mediaSecondaryInput_); |
| } |
| #endif |
| } else { |
| JAMI_WARN( |
| "Invalid conference state in attach participant: current \"%s\" - expected \"%s\"", |
| getStateStr(), |
| "ACTIVE_DETACHED"); |
| } |
| } |
| |
| void |
| Conference::detachLocalParticipant() |
| { |
| JAMI_INFO("Detach local participant from conference %s", id_.c_str()); |
| |
| if (getState() == State::ACTIVE_ATTACHED) { |
| foreachCall([&](auto call) { |
| Manager::instance().getRingBufferPool().unBindCallID(call->getCallId(), |
| RingBufferPool::DEFAULT_ID); |
| }); |
| |
| // Reset local audio source |
| hostAudioSource_ = {}; |
| |
| #ifdef ENABLE_VIDEO |
| if (videoMixer_) |
| videoMixer_->stopInput(); |
| |
| // Reset local video source |
| hostVideoSource_ = {}; |
| #endif |
| setState(State::ACTIVE_DETACHED); |
| } else { |
| JAMI_WARN( |
| "Invalid conference state in detach participant: current \"%s\" - expected \"%s\"", |
| getStateStr(), |
| "ACTIVE_ATTACHED"); |
| } |
| |
| setLocalHostDefaultMediaSource(); |
| } |
| |
| void |
| Conference::bindParticipant(const std::string& participant_id) |
| { |
| JAMI_INFO("Bind participant %s to conference %s", participant_id.c_str(), id_.c_str()); |
| |
| auto& rbPool = Manager::instance().getRingBufferPool(); |
| |
| for (const auto& item : getParticipantList()) { |
| if (participant_id != item) { |
| // Do not attach muted participants |
| if (auto call = Manager::instance().getCallFromCallID(item)) { |
| if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) |
| rbPool.bindHalfDuplexOut(item, participant_id); |
| else |
| rbPool.bindCallID(participant_id, item); |
| } |
| } |
| rbPool.flush(item); |
| } |
| |
| // Bind local participant to other participants only if the |
| // local is attached to the conference. |
| if (getState() == State::ACTIVE_ATTACHED) { |
| if (isMediaSourceMuted(MediaType::MEDIA_AUDIO)) |
| rbPool.bindHalfDuplexOut(RingBufferPool::DEFAULT_ID, participant_id); |
| else |
| rbPool.bindCallID(participant_id, RingBufferPool::DEFAULT_ID); |
| rbPool.flush(RingBufferPool::DEFAULT_ID); |
| } |
| } |
| |
| void |
| Conference::unbindParticipant(const std::string& participant_id) |
| { |
| JAMI_INFO("Unbind participant %s from conference %s", participant_id.c_str(), id_.c_str()); |
| Manager::instance().getRingBufferPool().unBindAllHalfDuplexOut(participant_id); |
| } |
| |
| void |
| Conference::bindHost() |
| { |
| JAMI_INFO("Bind host to conference %s", id_.c_str()); |
| |
| auto& rbPool = Manager::instance().getRingBufferPool(); |
| |
| for (const auto& item : getParticipantList()) { |
| if (auto call = Manager::instance().getCallFromCallID(item)) { |
| if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) |
| continue; |
| rbPool.bindCallID(item, RingBufferPool::DEFAULT_ID); |
| rbPool.flush(RingBufferPool::DEFAULT_ID); |
| } |
| } |
| } |
| |
| void |
| Conference::unbindHost() |
| { |
| JAMI_INFO("Unbind host from conference %s", id_.c_str()); |
| Manager::instance().getRingBufferPool().unBindAllHalfDuplexOut(RingBufferPool::DEFAULT_ID); |
| } |
| |
| ParticipantSet |
| Conference::getParticipantList() const |
| { |
| std::lock_guard<std::mutex> lk(participantsMtx_); |
| return participants_; |
| } |
| |
| bool |
| Conference::toggleRecording() |
| { |
| bool newState = not isRecording(); |
| if (newState) |
| initRecorder(recorder_); |
| else |
| deinitRecorder(recorder_); |
| |
| // Notify each participant |
| foreachCall([&](auto call) { call->updateRecState(newState); }); |
| |
| return Recordable::toggleRecording(); |
| } |
| |
| std::string |
| Conference::getAccountId() const |
| { |
| if (auto account = getAccount()) |
| return account->getAccountID(); |
| return {}; |
| } |
| |
| void |
| Conference::switchInput(const std::string& input) |
| { |
| #ifdef ENABLE_VIDEO |
| JAMI_DBG("[Conf:%s] Setting video input to %s", id_.c_str(), input.c_str()); |
| |
| hostVideoSource_.sourceUri_ = input; |
| |
| // Done if the video is disabled |
| if (not isVideoEnabled()) |
| return; |
| |
| if (auto mixer = videoMixer_) { |
| mixer->switchInput(input); |
| #ifdef ENABLE_PLUGIN |
| // Preview |
| if (auto& videoPreview = mixer->getVideoLocal()) { |
| auto previewSubject = std::make_shared<MediaStreamSubject>(pluginVideoMap_); |
| StreamData previewStreamData {getConfId(), false, StreamType::video, getConfId()}; |
| createConfAVStream(previewStreamData, *videoPreview, previewSubject, true); |
| } |
| #endif |
| } |
| #endif |
| } |
| |
| void |
| Conference::switchSecondaryInput(const std::string& input) |
| { |
| #ifdef ENABLE_VIDEO |
| mediaSecondaryInput_ = input; |
| if (videoMixer_) { |
| videoMixer_->switchSecondaryInput(input); |
| } |
| #endif |
| } |
| |
| bool |
| Conference::isVideoEnabled() const |
| { |
| if (auto shared = account_.lock()) |
| return shared->isVideoEnabled(); |
| return false; |
| } |
| |
| #ifdef ENABLE_VIDEO |
| std::shared_ptr<video::VideoMixer> |
| Conference::getVideoMixer() |
| { |
| return videoMixer_; |
| } |
| #endif |
| |
| void |
| Conference::initRecorder(std::shared_ptr<MediaRecorder>& rec) |
| { |
| // Video |
| if (videoMixer_) { |
| if (auto ob = rec->addStream(videoMixer_->getStream("v:mixer"))) { |
| videoMixer_->attach(ob); |
| } |
| } |
| |
| // Audio |
| // Create ghost participant for ringbufferpool |
| auto& rbPool = Manager::instance().getRingBufferPool(); |
| ghostRingBuffer_ = rbPool.createRingBuffer(getConfId()); |
| |
| // Bind it to ringbufferpool in order to get the all mixed frames |
| bindParticipant(getConfId()); |
| |
| // Add stream to recorder |
| audioMixer_ = jami::getAudioInput(getConfId()); |
| if (auto ob = rec->addStream(audioMixer_->getInfo("a:mixer"))) { |
| audioMixer_->attach(ob); |
| } |
| } |
| |
| void |
| Conference::deinitRecorder(std::shared_ptr<MediaRecorder>& rec) |
| { |
| // Video |
| if (videoMixer_) { |
| if (auto ob = rec->getStream("v:mixer")) { |
| videoMixer_->detach(ob); |
| } |
| } |
| |
| // Audio |
| if (auto ob = rec->getStream("a:mixer")) |
| audioMixer_->detach(ob); |
| audioMixer_.reset(); |
| Manager::instance().getRingBufferPool().unBindAll(getConfId()); |
| ghostRingBuffer_.reset(); |
| } |
| |
| void |
| Conference::onConfOrder(const std::string& callId, const std::string& confOrder) |
| { |
| // Check if the peer is a master |
| if (auto call = Manager::instance().getCallFromCallID(callId)) { |
| auto peerID = string_remove_suffix(call->getPeerNumber(), '@'); |
| if (!isModerator(peerID)) { |
| JAMI_WARN("Received conference order from a non master (%.*s)", |
| (int) peerID.size(), |
| peerID.data()); |
| return; |
| } |
| |
| std::string err; |
| Json::Value root; |
| Json::CharReaderBuilder rbuilder; |
| auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); |
| if (!reader->parse(confOrder.c_str(), confOrder.c_str() + confOrder.size(), &root, &err)) { |
| JAMI_WARN("Couldn't parse conference order from %.*s", |
| (int) peerID.size(), |
| peerID.data()); |
| return; |
| } |
| if (isVideoEnabled() and root.isMember("layout")) { |
| setLayout(root["layout"].asUInt()); |
| } |
| if (root.isMember("activeParticipant")) { |
| setActiveParticipant(root["activeParticipant"].asString()); |
| } |
| if (root.isMember("muteParticipant") and root.isMember("muteState")) { |
| muteParticipant(root["muteParticipant"].asString(), |
| root["muteState"].asString() == "true"); |
| } |
| if (root.isMember("hangupParticipant")) { |
| hangupParticipant(root["hangupParticipant"].asString()); |
| } |
| if (root.isMember("handRaised")) { |
| setHandRaised(root["handRaised"].asString(), root["handState"].asString() == "true"); |
| } |
| } |
| } |
| |
| std::shared_ptr<Call> |
| Conference::getCall(const std::string& callId) |
| { |
| return Manager::instance().callFactory.getCall(callId); |
| } |
| |
| bool |
| Conference::isModerator(std::string_view uri) const |
| { |
| return moderators_.find(uri) != moderators_.end() or isHost(uri); |
| } |
| |
| bool |
| Conference::isHandRaised(std::string_view uri) const |
| { |
| return isHost(uri) ? handsRaised_.find("host"sv) != handsRaised_.end() |
| : handsRaised_.find(uri) != handsRaised_.end(); |
| } |
| |
| void |
| Conference::setHandRaised(const std::string& participant_id, const bool& state) |
| { |
| if (isHost(participant_id)) { |
| auto isPeerRequiringAttention = isHandRaised("host"sv); |
| if (state and not isPeerRequiringAttention) { |
| JAMI_DBG("Raise host hand"); |
| handsRaised_.emplace("host"sv); |
| updateHandsRaised(); |
| } else if (not state and isPeerRequiringAttention) { |
| JAMI_DBG("Lower host hand"); |
| handsRaised_.erase("host"); |
| updateHandsRaised(); |
| } |
| return; |
| } else { |
| for (const auto& p : getParticipantList()) { |
| if (auto call = getCall(p)) { |
| auto isPeerRequiringAttention = isHandRaised(participant_id); |
| if (participant_id == string_remove_suffix(call->getPeerNumber(), '@')) { |
| if (state and not isPeerRequiringAttention) { |
| JAMI_DBG("Raise %s hand", participant_id.c_str()); |
| handsRaised_.emplace(participant_id); |
| updateHandsRaised(); |
| } else if (not state and isPeerRequiringAttention) { |
| JAMI_DBG("Remove %s raised hand", participant_id.c_str()); |
| handsRaised_.erase(participant_id); |
| updateHandsRaised(); |
| } |
| return; |
| } |
| } |
| } |
| } |
| JAMI_WARN("Fail to raise %s hand (participant not found)", participant_id.c_str()); |
| } |
| |
| void |
| Conference::setModerator(const std::string& participant_id, const bool& state) |
| { |
| for (const auto& p : getParticipantList()) { |
| if (auto call = getCall(p)) { |
| auto isPeerModerator = isModerator(participant_id); |
| if (participant_id == string_remove_suffix(call->getPeerNumber(), '@')) { |
| if (state and not isPeerModerator) { |
| JAMI_DBG("Add %s as moderator", participant_id.c_str()); |
| moderators_.emplace(participant_id); |
| updateModerators(); |
| } else if (not state and isPeerModerator) { |
| JAMI_DBG("Remove %s as moderator", participant_id.c_str()); |
| moderators_.erase(participant_id); |
| updateModerators(); |
| } |
| return; |
| } |
| } |
| } |
| JAMI_WARN("Fail to set %s as moderator (participant not found)", participant_id.c_str()); |
| } |
| |
| void |
| Conference::updateModerators() |
| { |
| std::lock_guard<std::mutex> lk(confInfoMutex_); |
| for (auto& info : confInfo_) { |
| info.isModerator = isModerator(string_remove_suffix(info.uri, '@')); |
| } |
| sendConferenceInfos(); |
| } |
| |
| void |
| Conference::updateHandsRaised() |
| { |
| std::lock_guard<std::mutex> lk(confInfoMutex_); |
| for (auto& info : confInfo_) { |
| info.handRaised = isHandRaised(string_remove_suffix(info.uri, '@')); |
| } |
| sendConferenceInfos(); |
| } |
| |
| void |
| Conference::foreachCall(const std::function<void(const std::shared_ptr<Call>& call)>& cb) |
| { |
| for (const auto& p : getParticipantList()) |
| if (auto call = getCall(p)) |
| cb(call); |
| } |
| |
| bool |
| Conference::isMuted(std::string_view uri) const |
| { |
| return participantsMuted_.find(uri) != participantsMuted_.end(); |
| } |
| |
| void |
| Conference::muteParticipant(const std::string& participant_id, const bool& state) |
| { |
| // Prioritize remote mute, otherwise the mute info is lost during |
| // the conference merge (we don't send back info to remoteHost, |
| // cf. getConfInfoHostUri method) |
| |
| // Transfert remote participant mute |
| auto remoteHost = findHostforRemoteParticipant(participant_id); |
| if (not remoteHost.empty()) { |
| if (auto call = getCallFromPeerID(string_remove_suffix(remoteHost, '@'))) { |
| auto w = call->getAccount(); |
| auto account = w.lock(); |
| if (!account) |
| return; |
| Json::Value root; |
| root["muteParticipant"] = participant_id; |
| root["muteState"] = state ? TRUE_STR : FALSE_STR; |
| call->sendConfOrder(root); |
| return; |
| } |
| } |
| |
| // Moderator mute host |
| if (isHost(participant_id)) { |
| auto isHostMuted = isMuted("host"sv); |
| if (state and not isHostMuted) { |
| participantsMuted_.emplace("host"sv); |
| if (not isMediaSourceMuted(MediaType::MEDIA_AUDIO)) { |
| JAMI_DBG("Mute host"); |
| unbindHost(); |
| } |
| } else if (not state and isHostMuted) { |
| participantsMuted_.erase("host"); |
| if (not isMediaSourceMuted(MediaType::MEDIA_AUDIO)) { |
| JAMI_DBG("Unmute host"); |
| bindHost(); |
| } |
| } |
| updateMuted(); |
| return; |
| } |
| |
| // Mute participant |
| if (auto call = getCallFromPeerID(participant_id)) { |
| auto isPartMuted = isMuted(participant_id); |
| if (state and not isPartMuted) { |
| JAMI_DBG("Mute participant %.*s", (int) participant_id.size(), participant_id.data()); |
| participantsMuted_.emplace(std::string(participant_id)); |
| unbindParticipant(call->getCallId()); |
| updateMuted(); |
| } else if (not state and isPartMuted) { |
| JAMI_DBG("Unmute participant %.*s", (int) participant_id.size(), participant_id.data()); |
| participantsMuted_.erase(std::string(participant_id)); |
| bindParticipant(call->getCallId()); |
| updateMuted(); |
| } |
| return; |
| } |
| } |
| |
| void |
| Conference::updateMuted() |
| { |
| std::lock_guard<std::mutex> lk(confInfoMutex_); |
| for (auto& info : confInfo_) { |
| auto peerID = string_remove_suffix(info.uri, '@'); |
| if (peerID.empty()) { |
| peerID = "host"sv; |
| info.audioModeratorMuted = isMuted(peerID); |
| info.audioLocalMuted = isMediaSourceMuted(MediaType::MEDIA_AUDIO); |
| } else { |
| info.audioModeratorMuted = isMuted(peerID); |
| if (auto call = getCallFromPeerID(peerID)) |
| info.audioLocalMuted = call->isPeerMuted(); |
| } |
| } |
| sendConferenceInfos(); |
| } |
| |
| ConfInfo |
| Conference::getConfInfoHostUri(std::string_view localHostURI, std::string_view destURI) |
| { |
| ConfInfo newInfo = confInfo_; |
| |
| for (auto it = newInfo.begin(); it != newInfo.end();) { |
| bool isRemoteHost = remoteHosts_.find(it->uri) != remoteHosts_.end(); |
| if (it->uri.empty() and not destURI.empty()) { |
| // fill the empty uri with the local host URI, let void for local client |
| it->uri = localHostURI; |
| } |
| if (isRemoteHost) { |
| // Don't send back the ParticipantInfo for remote Host |
| // For other than remote Host, the new info is in remoteHosts_ |
| it = newInfo.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| // Add remote Host info |
| for (const auto& [hostUri, confInfo] : remoteHosts_) { |
| // Add remote info for remote host destination |
| // Example: ConfA, ConfB & ConfC |
| // ConfA send ConfA and ConfB for ConfC |
| // ConfA send ConfA and ConfC for ConfB |
| // ... |
| if (destURI != hostUri) |
| newInfo.insert(newInfo.end(), confInfo.begin(), confInfo.end()); |
| } |
| return newInfo; |
| } |
| |
| bool |
| Conference::isHost(std::string_view uri) const |
| { |
| if (uri.empty()) |
| return true; |
| |
| // Check if the URI is a local URI (AccountID) for at least one of the subcall |
| // (a local URI can be in the call with another device) |
| for (const auto& p : getParticipantList()) { |
| if (auto call = getCall(p)) { |
| if (auto account = call->getAccount().lock()) { |
| if (account->getUsername() == uri) |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| void |
| Conference::updateConferenceInfo(ConfInfo confInfo) |
| { |
| std::lock_guard<std::mutex> lk(confInfoMutex_); |
| confInfo_ = std::move(confInfo); |
| sendConferenceInfos(); |
| } |
| |
| void |
| Conference::hangupParticipant(const std::string& participant_id) |
| { |
| if (isHost(participant_id)) { |
| Manager::instance().detachLocalParticipant(shared_from_this()); |
| return; |
| } |
| |
| if (auto call = getCallFromPeerID(participant_id)) { |
| if (auto account = call->getAccount().lock()) { |
| Manager::instance().hangupCall(account->getAccountID(), call->getCallId()); |
| } |
| return; |
| } |
| |
| // Transfert remote participant hangup |
| auto remoteHost = findHostforRemoteParticipant(participant_id); |
| if (remoteHost.empty()) { |
| JAMI_WARN("Can't hangup %s, peer not found", participant_id.c_str()); |
| return; |
| } |
| if (auto call = getCallFromPeerID(string_remove_suffix(remoteHost, '@'))) { |
| auto w = call->getAccount(); |
| auto account = w.lock(); |
| if (!account) |
| return; |
| |
| Json::Value root; |
| root["hangupParticipant"] = participant_id; |
| call->sendConfOrder(root); |
| return; |
| } |
| } |
| |
| void |
| Conference::muteLocalHost(bool is_muted, const std::string& mediaType) |
| { |
| if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_AUDIO) == 0) { |
| if (is_muted == isMediaSourceMuted(MediaType::MEDIA_AUDIO)) { |
| JAMI_DBG("Local audio source already in [%s] state", is_muted ? "muted" : "un-muted"); |
| return; |
| } |
| |
| auto isHostMuted = isMuted("host"sv); |
| if (is_muted and not isMediaSourceMuted(MediaType::MEDIA_AUDIO) and not isHostMuted) { |
| JAMI_DBG("Muting local audio source"); |
| unbindHost(); |
| } else if (not is_muted and isMediaSourceMuted(MediaType::MEDIA_AUDIO) and not isHostMuted) { |
| JAMI_DBG("Un-muting local audio source"); |
| bindHost(); |
| } |
| setLocalHostMuteState(MediaType::MEDIA_AUDIO, is_muted); |
| updateMuted(); |
| emitSignal<DRing::CallSignal::AudioMuted>(id_, is_muted); |
| return; |
| } else if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_VIDEO) == 0) { |
| #ifdef ENABLE_VIDEO |
| if (not isVideoEnabled()) { |
| JAMI_ERR("Cant't mute, the video is disabled!"); |
| return; |
| } |
| |
| if (is_muted == isMediaSourceMuted(MediaType::MEDIA_VIDEO)) { |
| JAMI_DBG("Local video source already in [%s] state", is_muted ? "muted" : "un-muted"); |
| return; |
| } |
| setLocalHostMuteState(MediaType::MEDIA_VIDEO, is_muted); |
| if (is_muted) { |
| if (auto mixer = videoMixer_) { |
| JAMI_DBG("Muting local video source"); |
| mixer->stopInput(); |
| } |
| } else { |
| if (auto mixer = videoMixer_) { |
| JAMI_DBG("Un-muting local video source"); |
| switchInput(hostVideoSource_.sourceUri_); |
| } |
| } |
| emitSignal<DRing::CallSignal::VideoMuted>(id_, is_muted); |
| return; |
| #endif |
| } |
| } |
| |
| void |
| Conference::resizeRemoteParticipants(ConfInfo& confInfo, std::string_view peerURI) |
| { |
| int remoteFrameHeight = confInfo.h; |
| int remoteFrameWidth = confInfo.w; |
| |
| if (remoteFrameHeight == 0 or remoteFrameWidth == 0) { |
| // get the size of the remote frame from receiveThread |
| // if the one from confInfo is empty |
| if (auto call = std::dynamic_pointer_cast<SIPCall>( |
| getCallFromPeerID(string_remove_suffix(peerURI, '@')))) { |
| if (auto const& videoRtp = call->getVideoRtp()) { |
| remoteFrameHeight = videoRtp->getVideoReceive()->getHeight(); |
| remoteFrameWidth = videoRtp->getVideoReceive()->getWidth(); |
| } |
| } |
| } |
| |
| if (remoteFrameHeight == 0 or remoteFrameWidth == 0) { |
| JAMI_WARN("Remote frame size not found."); |
| return; |
| } |
| |
| // get the size of the local frame |
| ParticipantInfo localCell; |
| for (const auto& p : confInfo_) { |
| if (p.uri == peerURI) { |
| localCell = p; |
| break; |
| } |
| } |
| |
| const float zoomX = (float) remoteFrameWidth / localCell.w; |
| const float zoomY = (float) remoteFrameHeight / localCell.h; |
| // Do the resize for each remote participant |
| for (auto& remoteCell : confInfo) { |
| remoteCell.x = remoteCell.x / zoomX + localCell.x; |
| remoteCell.y = remoteCell.y / zoomY + localCell.y; |
| remoteCell.w = remoteCell.w / zoomX; |
| remoteCell.h = remoteCell.h / zoomY; |
| } |
| } |
| |
| void |
| Conference::mergeConfInfo(ConfInfo& newInfo, const std::string& peerURI) |
| { |
| if (newInfo.empty()) { |
| JAMI_DBG("confInfo empty, remove remoteHost"); |
| std::lock_guard<std::mutex> lk(confInfoMutex_); |
| remoteHosts_.erase(peerURI); |
| sendConferenceInfos(); |
| return; |
| } |
| |
| resizeRemoteParticipants(newInfo, peerURI); |
| |
| bool updateNeeded = false; |
| auto it = remoteHosts_.find(peerURI); |
| if (it != remoteHosts_.end()) { |
| // Compare confInfo before update |
| if (it->second != newInfo) { |
| it->second = newInfo; |
| updateNeeded = true; |
| } else |
| JAMI_WARN("No change in confInfo, don't update"); |
| } else { |
| remoteHosts_.emplace(peerURI, newInfo); |
| updateNeeded = true; |
| } |
| // Send confInfo only if needed to avoid loops |
| if (updateNeeded and videoMixer_) { |
| // Trigger the layout update in the mixer because the frame resolution may |
| // change from participant to conference and cause a mismatch between |
| // confInfo layout and rendering layout. |
| videoMixer_->updateLayout(); |
| } |
| } |
| |
| std::string_view |
| Conference::findHostforRemoteParticipant(std::string_view uri) |
| { |
| for (const auto& host : remoteHosts_) { |
| for (const auto& p : host.second) { |
| if (uri == string_remove_suffix(p.uri, '@')) |
| return host.first; |
| } |
| } |
| return ""; |
| } |
| |
| std::shared_ptr<Call> |
| Conference::getCallFromPeerID(std::string_view peerID) |
| { |
| for (const auto& p : getParticipantList()) { |
| auto call = getCall(p); |
| if (call && string_remove_suffix(call->getPeerNumber(), '@') == peerID) { |
| return call; |
| } |
| } |
| return nullptr; |
| } |
| |
| } // namespace jami |