Files
jami-daemon/src/conference.cpp
Sébastien Blin 41a7ef1970 conference: fix crashes on leave
This patch fix several problems:
+ participants_ must be protected and not iterated while accessed
+ getVideoMixer() do not generate a video mixer, this is only
managed at one place.
+ Remove setConfId, enter/exitConf are here for this and this
is causing the videoMixer to not be destroyed in time, because
exitConference() was called after setConfId(""). This was causing
crashes because the videoMixer will update for the last participant
with the error: "The call is not bound to any conference"

GitLab: #660
Change-Id: Ic60bc7377b0315f7e2906ab03a7653381436180c
2021-11-15 11:55:17 -05:00

1360 lines
45 KiB
C++

/*
* 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(bool enableVideo)
: id_(Manager::instance().callFactory.getNewCallID())
#ifdef ENABLE_VIDEO
, videoEnabled_(enableVideo)
, mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
#endif
{
JAMI_INFO("Create new conference %s", id_.c_str());
#ifdef ENABLE_VIDEO
// We are done if the video is disabled.
if (not videoEnabled_)
return;
videoMixer_ = std::make_shared<video::VideoMixer>(id_);
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()
{
#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)
{
confState_ = state;
}
#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::setMediaSourceState(MediaType type, bool muted)
{
if (type == MediaType::MEDIA_AUDIO) {
audioSourceMuted_ = muted ? MediaSourceState::MUTED : MediaSourceState::UNMUTED;
} else if (type == MediaType::MEDIA_VIDEO) {
videoSourceMuted_ = muted ? MediaSourceState::MUTED : MediaSourceState::UNMUTED;
} else {
JAMI_ERR("Unsupported media type");
}
}
MediaSourceState
Conference::getMediaSourceState(MediaType type) const
{
if (type == MediaType::MEDIA_AUDIO) {
return audioSourceMuted_;
} else if (type == MediaType::MEDIA_VIDEO) {
return videoSourceMuted_;
} else {
JAMI_ERR("Unsupported media type");
return MediaSourceState::NONE;
}
}
bool
Conference::isMediaSourceMuted(MediaType type) const
{
if (type == MediaType::MEDIA_AUDIO) {
return audioSourceMuted_ == MediaSourceState::MUTED;
} else if (type == MediaType::MEDIA_VIDEO) {
return videoSourceMuted_ == MediaSourceState::MUTED;
} else {
JAMI_ERR("Unsupported media type");
return false;
}
}
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 media with
// a valid source type.
JAMI_DBG("[Call: %s] Does not have an active [%s] media source",
callId.c_str(),
MediaAttribute::mediaTypeToString(mediaType));
return;
}
if (getMediaSourceState(iter->type_) == MediaSourceState::NONE) {
// If the source state for the specified media type is not set
// yet, the state will initialized using the state of the first
// participant with a valid media source.
if (account->isRendezVous()) {
iter->muted_ = true;
}
setMediaSourceState(iter->type_, iter->muted_);
} else {
// To mute the local source, all the sources of the participating
// calls must be muted.
setMediaSourceState(iter->type_, iter->muted_ && 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);
}
}
}
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(call->getConfId());
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(), '@'));
}
// When a call joins a conference, the control if the media
// source sates (mainly mute/un-mute states) will be handled
// by the conference.
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(getConfID());
// 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)) {
auto videoRecv = call->getReceiveVideoFrameActiveWriter().get();
videoMixer_->setActiveParticipant(videoRecv);
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) {
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(mediaInput_);
if (not mediaSecondaryInput_.empty())
videoMixer_->switchSecondaryInput(mediaSecondaryInput_);
}
#endif
setMediaSourceState(MediaType::MEDIA_AUDIO, false);
setMediaSourceState(MediaType::MEDIA_VIDEO, false);
setState(State::ACTIVE_ATTACHED);
} 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);
});
#ifdef ENABLE_VIDEO
if (videoMixer_)
videoMixer_->stopInput();
#endif
setMediaSourceState(MediaType::MEDIA_AUDIO, true);
setMediaSourceState(MediaType::MEDIA_VIDEO, true);
setState(State::ACTIVE_DETACHED);
} else {
JAMI_WARN(
"Invalid conference state in detach participant: current \"%s\" - expected \"%s\"",
getStateStr(),
"ACTIVE_ATTACHED");
}
}
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();
}
const std::string&
Conference::getConfID() const
{
return id_;
}
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());
mediaInput_ = 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
{
return videoEnabled_;
}
#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(id_);
return;
}
if (auto call = getCallFromPeerID(participant_id)) {
Manager::instance().hangupCall(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();
}
setMediaSourceState(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;
}
setMediaSourceState(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(mediaInput_);
}
}
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