diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp index ecd6d9f7426e99c284f7457be53d3dae07bd1323..6c19f172af91e27a7fc48efbeaca5908d134adc2 100644 --- a/src/client/callmanager.cpp +++ b/src/client/callmanager.cpp @@ -71,10 +71,13 @@ requestMediaChange(const std::string& accountId, const std::string& callId, const std::vector<DRing::MediaMap>& mediaList) { - if (auto account = jami::Manager::instance().getAccount(accountId)) + if (auto account = jami::Manager::instance().getAccount(accountId)) { if (auto call = account->getCall(callId)) { return call->requestMediaChange(mediaList); + } else if (auto conf = account->getConference(callId)) { + return conf->requestMediaChange(mediaList); } + } return false; } @@ -147,13 +150,15 @@ muteLocalMedia(const std::string& accountId, { if (auto account = jami::Manager::instance().getAccount(accountId)) { if (auto call = account->getCall(callId)) { + JAMI_DBG("Muting [%s] for call %s", mediaType.c_str(), callId.c_str()); call->muteMedia(mediaType, mute); return true; } else if (auto conf = account->getConference(callId)) { + JAMI_DBG("Muting local host [%s] for conference %s", mediaType.c_str(), callId.c_str()); conf->muteLocalHost(mute, mediaType); return true; } else { - JAMI_DBG("CallID %s doesn't exist in call muting", callId.c_str()); + JAMI_WARN("ID %s doesn't match any call or conference", callId.c_str()); } } return false; diff --git a/src/conference.cpp b/src/conference.cpp index 3f29909be850d19411dd013689ab0298e2372e4b..14c0cf7cc2489d5057bb2dcf84f3bc0a2d44e011 100644 --- a/src/conference.cpp +++ b/src/conference.cpp @@ -57,17 +57,40 @@ Conference::Conference(const std::shared_ptr<Account>& account) , account_(account) #ifdef ENABLE_VIDEO , videoEnabled_(account->isVideoEnabled()) - , mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice()) #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_, mediaInput_); + 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(); @@ -143,6 +166,8 @@ Conference::Conference(const std::shared_ptr<Account>& account) Conference::~Conference() { + JAMI_INFO("Destroying conference %s", id_.c_str()); + #ifdef ENABLE_VIDEO foreachCall([&](auto call) { call->exitConference(); @@ -197,9 +222,51 @@ Conference::getState() const 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() @@ -259,41 +326,39 @@ Conference::createConfAVStream(const StreamData& StreamData, #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 +Conference::setLocalHostMuteState(MediaType type, bool muted) { if (type == MediaType::MEDIA_AUDIO) { - return audioSourceMuted_; + hostAudioSource_.muted_ = muted; } else if (type == MediaType::MEDIA_VIDEO) { - return videoSourceMuted_; + hostVideoSource_.muted_ = muted; } 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 { + 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 false; + 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 @@ -324,26 +389,23 @@ Conference::takeOverMediaSourceControl(const std::string& callId) 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. + // 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)); - return; + continue; } - 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 { + if (getState() == State::ACTIVE_ATTACHED) { // To mute the local source, all the sources of the participating - // calls must be muted. - setMediaSourceState(iter->type_, iter->muted_ && isMediaSourceMuted(iter->type_)); + // 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 @@ -372,6 +434,83 @@ Conference::takeOverMediaSourceControl(const std::string& callId) } } +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) @@ -422,9 +561,9 @@ Conference::addParticipant(const std::string& participant_id) 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. + // 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); } @@ -637,6 +776,9 @@ 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)) { @@ -654,14 +796,11 @@ Conference::attachLocalParticipant() #ifdef ENABLE_VIDEO if (videoMixer_) { - videoMixer_->switchInput(mediaInput_); + videoMixer_->switchInput(hostVideoSource_.sourceUri_); 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\"", @@ -680,12 +819,17 @@ Conference::detachLocalParticipant() 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 - setMediaSourceState(MediaType::MEDIA_AUDIO, true); - setMediaSourceState(MediaType::MEDIA_VIDEO, true); setState(State::ACTIVE_DETACHED); } else { JAMI_WARN( @@ -693,6 +837,8 @@ Conference::detachLocalParticipant() getStateStr(), "ACTIVE_ATTACHED"); } + + setLocalHostDefaultMediaSource(); } void @@ -793,7 +939,7 @@ 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; + hostVideoSource_.sourceUri_ = input; // Done if the video is disabled if (not isVideoEnabled()) @@ -1228,7 +1374,7 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType) JAMI_DBG("Un-muting local audio source"); bindHost(); } - setMediaSourceState(MediaType::MEDIA_AUDIO, is_muted); + setLocalHostMuteState(MediaType::MEDIA_AUDIO, is_muted); updateMuted(); emitSignal<DRing::CallSignal::AudioMuted>(id_, is_muted); return; @@ -1239,11 +1385,11 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType) return; } - if (is_muted == (isMediaSourceMuted(MediaType::MEDIA_VIDEO))) { + 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); + setLocalHostMuteState(MediaType::MEDIA_VIDEO, is_muted); if (is_muted) { if (auto mixer = videoMixer_) { JAMI_DBG("Muting local video source"); @@ -1252,7 +1398,7 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType) } else { if (auto mixer = videoMixer_) { JAMI_DBG("Un-muting local video source"); - switchInput(mediaInput_); + switchInput(hostVideoSource_.sourceUri_); } } emitSignal<DRing::CallSignal::VideoMuted>(id_, is_muted); diff --git a/src/conference.h b/src/conference.h index 8cf1bcbcc38161238db490a5cb07febc3f0cf596..ba8a45ef7bc1e51487a7d1690a886ccf10cd3ee3 100644 --- a/src/conference.h +++ b/src/conference.h @@ -33,6 +33,7 @@ #include <functional> #include "audio/audio_input.h" +#include "media_attribute.h" #include <json/json.h> @@ -172,12 +173,6 @@ struct ConfInfo : public std::vector<ParticipantInfo> std::string toString() const; }; -enum class MediaSourceState : unsigned { - NONE = 0, // Not set yet - MUTED, - UNMUTED -}; - using ParticipantSet = std::set<std::string>; class Conference : public Recordable, public std::enable_shared_from_this<Conference> @@ -233,15 +228,19 @@ public: const char* getStateStr() const { return getStateStr(confState_); } + /** + * Set default media source for the local host + */ + void setLocalHostDefaultMediaSource(); + /** * Set the mute state of the local host */ - void setMediaSourceState(MediaType type, bool muted); + void setLocalHostMuteState(MediaType type, bool muted); /** * Get the mute state of the local host */ - MediaSourceState getMediaSourceState(MediaType type) const; bool isMediaSourceMuted(MediaType type) const; /** @@ -253,7 +252,16 @@ public: void takeOverMediaSourceControl(const std::string& callId); /** - * Process incoming media change request. + * Process a media change request. + * Used to change the media attributes of the host. + * + * @param remoteMediaList new media list from the remote + * @return true on success + */ + bool requestMediaChange(const std::vector<DRing::MediaMap>& mediaList); + + /** + * Process incoming media change request. * * @param callId the call ID * @param remoteMediaList new media list from the remote @@ -326,7 +334,7 @@ public: #ifdef ENABLE_VIDEO std::shared_ptr<video::VideoMixer> getVideoMixer(); - std::string getVideoInput() const { return mediaInput_; } + std::string getVideoInput() const { return hostVideoSource_.sourceUri_; } #endif std::vector<std::map<std::string, std::string>> getConferenceInfos() const @@ -377,7 +385,6 @@ private: #ifdef ENABLE_VIDEO bool videoEnabled_; - std::string mediaInput_ {}; std::string mediaSecondaryInput_ {}; std::shared_ptr<video::VideoMixer> videoMixer_; std::map<std::string, std::shared_ptr<video::SinkClient>> confSinksMap_ {}; @@ -405,11 +412,12 @@ private: * Currently, the conference and the client support only one stream * per media type, even if the call supports an arbitrary number of * streams per media type. Thus, these two variables will hold the - * media source states regardless of the media type (capture device, - * display, ...) + * current media source attributes */ - MediaSourceState audioSourceMuted_ {MediaSourceState::NONE}; - MediaSourceState videoSourceMuted_ {MediaSourceState::NONE}; + MediaAttribute hostAudioSource_ {}; +#ifdef ENABLE_VIDEO + MediaAttribute hostVideoSource_ {}; +#endif bool localModAdded_ {false}; diff --git a/src/manager.cpp b/src/manager.cpp index 86138ba947666c57eda634e1b72b9fc57cd4aaaf..01c54c1754914cdf4bbfb047d0d8bb42f4c570e1 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1409,6 +1409,11 @@ Manager::joinParticipant(const std::string& accountId, return false; } + JAMI_INFO("Creating conference for participants %s and %s. Attach host [%s]", + callId1.c_str(), + callId2.c_str(), + attached ? "YES" : "NO"); + if (callId1 == callId2) { JAMI_ERR("Cannot join participant %s to itself", callId1.c_str()); return false; diff --git a/src/media/media_attribute.cpp b/src/media/media_attribute.cpp index 3c9cc03a30745ad70baa79f21754d8c486994aca..4060ae414943fa225eba664cc0bae502606afb86 100644 --- a/src/media/media_attribute.cpp +++ b/src/media/media_attribute.cpp @@ -224,7 +224,6 @@ std::string MediaAttribute::toString(bool full) const { std::ostringstream descr; - descr << "[" << this << "] "; descr << "type " << (type_ == MediaType::MEDIA_AUDIO ? "[AUDIO]" : "[VIDEO]"); descr << " "; descr << "enabled " << (enabled_ ? "[YES]" : "[NO]"); diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp index b8e030e412edc4e4c0773ac54b4fba30c242fc80..36d964671eece77e085d3969a24576b6638dbb71 100644 --- a/src/media/video/video_mixer.cpp +++ b/src/media/video/video_mixer.cpp @@ -263,6 +263,11 @@ VideoMixer::process() if (delay.count() > 0) std::this_thread::sleep_for(delay); + // Nothing to do. + if (width_ == 0 or height_ == 0) { + return; + } + VideoFrame& output = getNewFrame(); try { output.reserve(format_, width_, height_);