From b370ada71114f052fed6d966fe3c88f46ad674a6 Mon Sep 17 00:00:00 2001
From: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
Date: Thu, 30 Sep 2021 16:30:56 -0400
Subject: [PATCH] multi-stream: handle media change request in conference

Handle media change request in conference, specifically when adding
video to a an audio only call.

Gitlab: #638

Change-Id: I0eb892eb941d2a62b6046c7b2ac9d128f4bcbd12
---
 src/call.h                            |  44 +++++++-
 src/conference.cpp                    |  59 +++++++++-
 src/conference.h                      |   9 ++
 src/ice_transport.cpp                 |   6 +-
 src/manager.cpp                       |  19 +---
 src/manager.h                         |  11 +-
 src/media/media_attribute.cpp         |   4 -
 src/media/video/video_mixer.cpp       |   4 +-
 src/media/video/video_rtp_session.cpp |   8 +-
 src/sip/sipcall.cpp                   | 153 +++++++++++++++++++++++---
 src/sip/sipcall.h                     |   6 +-
 11 files changed, 258 insertions(+), 65 deletions(-)

diff --git a/src/call.h b/src/call.h
index 8e4e930c30..3f817b4c46 100644
--- a/src/call.h
+++ b/src/call.h
@@ -219,9 +219,33 @@ public:
     virtual void answer(const std::vector<DRing::MediaMap>& mediaList) = 0;
 
     /**
-     * Answer to a media update request. The media attributes set by the
-     * caller of this method will determine the response sent to the
-     * peer and the configuration of the local media.
+     * Check the media of an incoming media change request.
+     * This method checks the new media against the current media. It
+     * determines if the differences are significant enough to require
+     * more processing.
+     * For instance, this can be used to check if the a change request
+     * must be reported to the client for confirmation or can be handled
+     * by the daemon.
+     * The conditions that cause this method to return true are implementation
+     * specific.
+     *
+     * @param the new media list from the remote
+     * @return true if the new media differs from the current media
+     **/
+    virtual bool checkMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList) = 0;
+
+    /**
+     * Process incoming media change request.
+     *
+     * @param the new media list from the remote
+     */
+    virtual void handleMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList) = 0;
+
+    /**
+     * Answer to a media update request.
+     * The media attributes set by the caller of this method will
+     * determine the response to send to the peer and the configuration
+     * of the local media.
      * @param mediaList The list of media attributes. An empty media
      * list means the media update request was not accepted, meaning the
      * call continue with the current media. It's up to the implementation
@@ -318,11 +342,23 @@ public:
                                                                            - duration_start_);
     }
 
-public: // media management
+    // media management
     virtual bool toggleRecording();
 
     virtual std::vector<MediaAttribute> getMediaAttributeList() const = 0;
 
+    /**
+     * Add a dummy video stream with the attached sink.
+     * Typically needed in conference to display infos for participants
+     * that have joined the conference without video (audio only).
+     */
+    virtual bool addDummyVideoRtpSession() = 0;
+
+    /**
+     * Remove all dummy video streams.
+     */
+    virtual void removeDummyVideoRtpSessions() = 0;
+
     virtual void switchInput(const std::string& = {}) {};
 
     /**
diff --git a/src/conference.cpp b/src/conference.cpp
index c4a4a75129..3da528b715 100644
--- a/src/conference.cpp
+++ b/src/conference.cpp
@@ -55,8 +55,8 @@ namespace jami {
 Conference::Conference(bool enableVideo)
     : id_(Manager::instance().callFactory.getNewCallID())
 #ifdef ENABLE_VIDEO
-    , mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
     , videoEnabled_(enableVideo)
+    , mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
 #endif
 {
     JAMI_INFO("Create new conference %s", id_.c_str());
@@ -128,7 +128,6 @@ Conference::Conference(bool enableVideo)
             }
             lk.unlock();
             if (!hostAdded) {
-                auto audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO);
                 ParticipantInfo pi;
                 pi.videoMuted = true;
                 pi.audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO);
@@ -310,6 +309,12 @@ Conference::takeOverMediaSourceControl(const std::string& callId)
         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};
@@ -323,7 +328,7 @@ 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 that uses with
+            // 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(),
@@ -335,7 +340,7 @@ Conference::takeOverMediaSourceControl(const std::string& callId)
             // 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 (call->getAccount().lock()->isRendezVous()) {
+            if (account->isRendezVous()) {
                 iter->muted_ = true;
             }
             setMediaSourceState(iter->type_, iter->muted_);
@@ -371,6 +376,39 @@ Conference::takeOverMediaSourceControl(const std::string& callId)
     }
 }
 
+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)
 {
@@ -415,6 +453,14 @@ Conference::addParticipant(const std::string& participant_id)
         }
 #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()) {
@@ -750,6 +796,8 @@ 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
@@ -877,8 +925,7 @@ Conference::onConfOrder(const std::string& callId, const std::string& confOrder)
             hangupParticipant(root["hangupParticipant"].asString());
         }
         if (root.isMember("handRaised")) {
-            setHandRaised(root["handRaised"].asString(),
-                                 root["handState"].asString() == "true");
+            setHandRaised(root["handRaised"].asString(), root["handState"].asString() == "true");
         }
     }
 }
diff --git a/src/conference.h b/src/conference.h
index 424fa4e638..2bcdb64726 100644
--- a/src/conference.h
+++ b/src/conference.h
@@ -247,6 +247,15 @@ public:
      */
     void takeOverMediaSourceControl(const std::string& callId);
 
+    /**
+     *  Process incoming media change request.
+     *
+     * @param callId the call ID
+     * @param remoteMediaList new media list from the remote
+     */
+    void handleMediaChangeRequest(const std::shared_ptr<Call>& call,
+                                  const std::vector<DRing::MediaMap>& remoteMediaList);
+
     /**
      * Add a new participant to the conference
      */
diff --git a/src/ice_transport.cpp b/src/ice_transport.cpp
index 293ba0864e..060311c13d 100644
--- a/src/ice_transport.cpp
+++ b/src/ice_transport.cpp
@@ -732,7 +732,7 @@ IceTransport::Impl::link() const
             auto laddr = getLocalAddress(absIdx);
             auto raddr = getRemoteAddress(absIdx);
 
-            if (laddr and raddr) {
+            if (laddr and laddr.getPort() != 0 and raddr and raddr.getPort() != 0) {
                 out << " [" << i << "] " << laddr.toString(true, true) << " ["
                     << getCandidateType(getSelectedCandidate(absIdx, false)) << "] "
                     << " <-> " << raddr.toString(true, true) << " ["
@@ -811,7 +811,7 @@ IceTransport::Impl::getSelectedCandidate(unsigned comp_id, bool remote) const
 
     const auto* sess = pj_ice_strans_get_valid_pair(icest_, comp_id);
     if (sess == nullptr) {
-        JAMI_ERR("[ice:%p] Component %i has no valid pair", this, comp_id);
+        JAMI_WARN("[ice:%p] Component %i has no valid pair (disabled)", this, comp_id);
         return nullptr;
     }
 
@@ -829,7 +829,6 @@ IceTransport::Impl::getLocalAddress(unsigned comp_id) const
     if (auto cand = getSelectedCandidate(comp_id, false))
         return cand->addr;
 
-    JAMI_ERR("[ice:%p] No local address for component %i", this, comp_id);
     return {};
 }
 
@@ -841,7 +840,6 @@ IceTransport::Impl::getRemoteAddress(unsigned comp_id) const
     if (auto cand = getSelectedCandidate(comp_id, true))
         return cand->addr;
 
-    JAMI_ERR("[ice:%p] No remote address for component %i", this, comp_id);
     return {};
 }
 
diff --git a/src/manager.cpp b/src/manager.cpp
index fd8dc23b0a..552e40aa2d 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -52,7 +52,6 @@ using random_device = dht::crypto::random_device;
 
 #include "sip/sip_utils.h"
 #include "sip/sipvoiplink.h"
-#include "sip/sipaccount.h"
 
 #include "im/instant_messaging.h"
 
@@ -1664,7 +1663,8 @@ Manager::createConfFromParticipantList(const std::vector<std::string>& participa
     bool videoEnabled {false};
     for (const auto& numberaccount : participantList) {
         std::string tostr(numberaccount.substr(0, numberaccount.find(',')));
-        std::string accountId(numberaccount.substr(numberaccount.find(',') + 1, numberaccount.size()));
+        std::string accountId(
+            numberaccount.substr(numberaccount.find(',') + 1, numberaccount.size()));
         auto account = getAccount(accountId);
         if (account) {
             videoEnabled |= account->isVideoEnabled();
@@ -2107,20 +2107,6 @@ Manager::incomingCall(Call& call, const std::string& accountId)
     pimpl_->processIncomingCall(call, accountId);
 }
 
-void
-Manager::mediaChangeRequested(const std::string& callId,
-                              const std::string& accountId,
-                              const std::vector<DRing::MediaMap>& mediaList)
-{
-    JAMI_INFO("Media change request for call %s on account %s with %lu media",
-              callId.c_str(),
-              accountId.c_str(),
-              mediaList.size());
-
-    // Report the media change request.
-    emitSignal<DRing::CallSignal::MediaChangeRequested>(accountId, callId, mediaList);
-}
-
 void
 Manager::incomingMessage(const std::string& callID,
                          const std::string& from,
@@ -2883,7 +2869,6 @@ Manager::ManagerPimpl::processIncomingCall(Call& incomCall, const std::string& a
     }
 }
 
-
 AudioFormat
 Manager::hardwareAudioFormatChanged(AudioFormat format)
 {
diff --git a/src/manager.h b/src/manager.h
index 464d8ba711..08e725da0b 100644
--- a/src/manager.h
+++ b/src/manager.h
@@ -55,7 +55,7 @@ namespace jami {
 namespace video {
 class SinkClient;
 class VideoGenerator;
-}
+} // namespace video
 class ChannelSocket;
 class RingBufferPool;
 struct VideoManager;
@@ -214,15 +214,6 @@ public:
      */
     void incomingCall(Call& call, const std::string& accountId);
 
-    /**
-     * Handle a media change request from the peer
-     * @param callId
-     * @param accountId
-     */
-    void mediaChangeRequested(const std::string& callId,
-                              const std::string& accountId,
-                              const std::vector<DRing::MediaMap>& mediaList);
-
     /**
      * Functions which occur with a user's action
      * Hangup the call
diff --git a/src/media/media_attribute.cpp b/src/media/media_attribute.cpp
index a8b1784846..3c9cc03a30 100644
--- a/src/media/media_attribute.cpp
+++ b/src/media/media_attribute.cpp
@@ -87,7 +87,6 @@ MediaAttribute::getMediaType(const DRing::MediaMap& map)
 {
     const auto& iter = map.find(DRing::Media::MediaAttributeKey::MEDIA_TYPE);
     if (iter == map.end()) {
-        JAMI_WARN("[MEDIA_TYPE] key not found in media map");
         return {false, MediaType::MEDIA_NONE};
     }
 
@@ -117,7 +116,6 @@ MediaAttribute::getMediaSourceType(const DRing::MediaMap& map)
 {
     const auto& iter = map.find(DRing::Media::MediaAttributeKey::SOURCE_TYPE);
     if (iter == map.end()) {
-        JAMI_WARN("[MEDIA_TYPE] key not found in media map");
         return {false, MediaSourceType::NONE};
     }
 
@@ -129,7 +127,6 @@ MediaAttribute::getBoolValue(const DRing::MediaMap& map, const std::string& key)
 {
     const auto& iter = map.find(key);
     if (iter == map.end()) {
-        JAMI_WARN("[%s] key not found for media", key.c_str());
         return {false, false};
     }
 
@@ -148,7 +145,6 @@ MediaAttribute::getStringValue(const DRing::MediaMap& map, const std::string& ke
 {
     const auto& iter = map.find(key);
     if (iter == map.end()) {
-        JAMI_WARN("[%s] key not found in media map", key.c_str());
         return {false, {}};
     }
 
diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp
index 9271534a7d..0540436d50 100644
--- a/src/media/video/video_mixer.cpp
+++ b/src/media/video/video_mixer.cpp
@@ -117,6 +117,8 @@ VideoMixer::~VideoMixer()
 void
 VideoMixer::switchInput(const std::string& input)
 {
+    JAMI_DBG("Set new input %s", input.c_str());
+
     if (auto local = videoLocal_) {
         // Detach videoInput from mixer
         local->detach(this);
@@ -131,7 +133,7 @@ VideoMixer::switchInput(const std::string& input)
     }
 
     if (input.empty()) {
-        JAMI_DBG("[mixer:%s] Input is empty, don't add it in the mixer", id_.c_str());
+        JAMI_DBG("[mixer:%s] Input is empty, don't add it to the mixer", id_.c_str());
         return;
     }
 
diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp
index 124987d5a8..8c23e70d8c 100644
--- a/src/media/video/video_rtp_session.cpp
+++ b/src/media/video/video_rtp_session.cpp
@@ -71,11 +71,13 @@ VideoRtpSession::VideoRtpSession(const string& callID, const DeviceParams& local
 {
     setupVideoBitrateInfo(); // reset bitrate
     cc = std::make_unique<CongestionControl>();
+    JAMI_DBG("[%p] Video RTP session created", this);
 }
 
 VideoRtpSession::~VideoRtpSession()
 {
     stop();
+    JAMI_DBG("[%p] Video RTP session destroyed", this);
 }
 
 /// Setup internal VideoBitrateInfo structure from media descriptors.
@@ -97,7 +99,7 @@ void
 VideoRtpSession::startSender()
 {
     JAMI_DBG("Start video RTP sender: input [%s] - muted [%s]",
-             input_.c_str(),
+             conference_ ? "Video Mixer" : input_.c_str(),
              muteState_ ? "YES" : "NO");
 
     if (send_.enabled and not send_.onHold) {
@@ -321,8 +323,10 @@ VideoRtpSession::setupVideoPipeline()
     if (conference_)
         setupConferenceVideoPipeline(*conference_);
     else if (sender_) {
-        if (videoLocal_)
+        if (videoLocal_) {
+            JAMI_DBG("[call:%s] Setup video pipeline on local capture device", callID_.c_str());
             videoLocal_->attach(sender_.get());
+        }
     } else {
         videoLocal_.reset();
     }
diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp
index d7847892d9..c6667cd0f5 100644
--- a/src/sip/sipcall.cpp
+++ b/src/sip/sipcall.cpp
@@ -88,6 +88,8 @@ static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR = "10.0.2"sv;
 static const std::vector<unsigned> MULTISTREAM_REQUIRED_VERSION
     = split_string_to_unsigned(MULTISTREAM_REQUIRED_VERSION_STR, '.');
 
+constexpr auto DUMMY_VIDEO_STR = "dummy video session";
+
 SIPCall::SIPCall(const std::shared_ptr<SIPAccountBase>& account,
                  const std::string& callId,
                  Call::CallType type,
@@ -933,6 +935,16 @@ SIPCall::answerMediaChangeRequest(const std::vector<DRing::MediaMap>& mediaList)
 
     auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
 
+    // TODO. is the right place?
+    // Disable video if disabled in the account.
+    if (not account->isVideoEnabled()) {
+        for (auto& mediaAttr : mediaAttrList) {
+            if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
+                mediaAttr.enabled_ = false;
+            }
+        }
+    }
+
     if (mediaAttrList.empty()) {
         JAMI_DBG("[call:%s] Media list size is empty. Ignoring the media change request",
                  getCallId().c_str());
@@ -2218,7 +2230,7 @@ SIPCall::updateMediaStream(const MediaAttribute& newMediaAttr, size_t streamIdx)
 void
 SIPCall::updateAllMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
 {
-    JAMI_DBG("[call:%s] New local medias", getCallId().c_str());
+    JAMI_DBG("[call:%s] New local media", getCallId().c_str());
 
     unsigned idx = 0;
     for (auto const& newMediaAttr : mediaAttrList) {
@@ -2284,6 +2296,26 @@ SIPCall::requestMediaChange(const std::vector<DRing::MediaMap>& mediaList)
 {
     auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, isSrtpEnabled());
 
+    // TODO. is the right place?
+    // Disable video if disabled in the account.
+    auto account = getSIPAccount();
+    if (not account) {
+        JAMI_ERR("[call:%s] No account detected", getCallId().c_str());
+        return false;
+    }
+    if (not account->isVideoEnabled()) {
+        for (auto& mediaAttr : mediaAttrList) {
+            if (mediaAttr.type_ == MediaType::MEDIA_VIDEO) {
+                // This an API misuse. The new medialist should not contain video
+                // if it was disabled in the account settings.
+                JAMI_ERR("[call:%s] New media has video, but it's disabled in the account. "
+                         "Ignoring the change request!",
+                         getCallId().c_str());
+                return false;
+            }
+        }
+    }
+
     // If the peer does not support multi-stream and the size of the new
     // media list is different from the current media list, the media
     // change request will be ignored.
@@ -2440,6 +2472,59 @@ SIPCall::onIceNegoSucceed()
     startAllMedia();
 }
 
+bool
+SIPCall::checkMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList)
+{
+    // The current media is considered to have changed if one of the
+    // following condtions is true:
+    //
+    // - the number of media changed
+    // - the type of one of the media changed (unlikely)
+    // - one of the media was enabled/disabled
+
+    JAMI_DBG("[call:%s] Received a media change request", getCallId().c_str());
+
+    auto remoteMediaAtrrList = MediaAttribute::buildMediaAttributesList(remoteMediaList,
+                                                                        isSrtpEnabled());
+    if (remoteMediaAtrrList.size() != rtpStreams_.size())
+        return true;
+
+    for (size_t i = 0; i < rtpStreams_.size(); i++) {
+        if (remoteMediaAtrrList[i].type_ != rtpStreams_[i].mediaAttribute_->type_)
+            return true;
+        if (remoteMediaAtrrList[i].enabled_ != rtpStreams_[i].mediaAttribute_->enabled_)
+            return true;
+    }
+
+    return false;
+}
+
+void
+SIPCall::handleMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList)
+{
+    JAMI_DBG("[call:%s] Handling media change request", getCallId().c_str());
+
+    auto account = getAccount().lock();
+    if (not account) {
+        JAMI_ERR("No account detected");
+        return;
+    }
+
+    // If multi-stream is supported and the offered media differ from
+    // the current media, the request is reported to the client to be
+    // processed. Otherwise, we answer with the current local media.
+
+    if (account->isMultiStreamEnabled() and checkMediaChangeRequest(remoteMediaList)) {
+        // Report the media change request.
+        emitSignal<DRing::CallSignal::MediaChangeRequested>(getAccountId(),
+                                                            getCallId(),
+                                                            remoteMediaList);
+    } else {
+        auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
+        answerMediaChangeRequest(localMediaList);
+    }
+}
+
 pj_status_t
 SIPCall::onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdata)
 {
@@ -2476,20 +2561,17 @@ SIPCall::onReceiveReinvite(const pjmedia_sdp_session* offer, pjsip_rx_data* rdat
     pjsip_tx_data* tdata = nullptr;
     if (pjsip_inv_initial_answer(inviteSession_.get(), rdata, PJSIP_SC_TRYING, NULL, NULL, &tdata)
         != PJ_SUCCESS) {
-        JAMI_ERR("Could not create answer TRYING");
+        JAMI_ERR("[call:%s] Could not create answer TRYING", getCallId().c_str());
         return res;
     }
 
     // Report the change request.
     auto const& remoteMediaList = MediaAttribute::mediaAttributesToMediaMaps(mediaAttrList);
-    // TODO_MC. Validate this assessment.
-    // Report re-invites only if the number of media changed, otherwise answer
-    // using the current local attributes.
-    if (acc->isMultiStreamEnabled() and remoteMediaList.size() != rtpStreams_.size()) {
-        Manager::instance().mediaChangeRequested(getCallId(), getAccountId(), remoteMediaList);
+
+    if (auto conf = Manager::instance().getConferenceFromCallID(getCallId())) {
+        conf->handleMediaChangeRequest(shared_from_this(), remoteMediaList);
     } else {
-        auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
-        answerMediaChangeRequest(localMediaList);
+        handleMediaChangeRequest(remoteMediaList);
     }
 
     return res;
@@ -2719,10 +2801,12 @@ SIPCall::getDetails() const
 void
 SIPCall::enterConference(const std::string& confId)
 {
+    JAMI_DBG("[call:%s] Entering conference [%s]", getCallId().c_str(), confId.c_str());
+
 #ifdef ENABLE_VIDEO
     auto conf = Manager::instance().getConferenceFromID(confId);
     if (conf == nullptr) {
-        JAMI_ERR("Unknown conference [%s]", confId.c_str());
+        JAMI_ERR("[call:%s] Unknown conference [%s]", getCallId().c_str(), confId.c_str());
         return;
     }
 
@@ -2745,6 +2829,14 @@ SIPCall::enterConference(const std::string& confId)
 void
 SIPCall::exitConference()
 {
+    auto confId = getConfId();
+    if (not confId.empty()) {
+        JAMI_DBG("[call:%s] Leaving conference [%s]", getCallId().c_str(), confId.c_str());
+    } else {
+        JAMI_ERR("[call:%s] The call is not bound to any conference", getCallId().c_str());
+        return;
+    }
+
     auto const& audioRtp = getAudioRtp();
     if (audioRtp && !isCaptureDeviceMuted(MediaType::MEDIA_AUDIO)) {
         auto& rbPool = Manager::instance().getRingBufferPool();
@@ -2773,20 +2865,49 @@ SIPCall::getReceiveVideoFrameActiveWriter()
     return {};
 }
 
-std::shared_ptr<video::VideoRtpSession>
+bool
 SIPCall::addDummyVideoRtpSession()
 {
 #ifdef ENABLE_VIDEO
-    MediaAttribute mediaAttr(MediaType::MEDIA_VIDEO, true, true, false, "", "dummy video session");
+    JAMI_DBG("[call:%s] Add dummy video stream", getCallId().c_str());
+
+    MediaAttribute mediaAttr(MediaType::MEDIA_VIDEO,
+                             true,
+                             true,
+                             false,
+                             "dummy source",
+                             DUMMY_VIDEO_STR);
+
     addMediaStream(mediaAttr);
     auto& stream = rtpStreams_.back();
     createRtpSession(stream);
-    if (stream.rtpSession_) {
-        return std::dynamic_pointer_cast<video::VideoRtpSession>(stream.rtpSession_);
-    }
+    return stream.rtpSession_ != nullptr;
 #endif
 
-    return {};
+    return false;
+}
+
+void
+SIPCall::removeDummyVideoRtpSessions()
+{
+    // It's not expected to have more than one dummy video stream, but
+    // check just in case.
+    auto removed = std::remove_if(rtpStreams_.begin(),
+                                  rtpStreams_.end(),
+                                  [](const RtpStream& stream) {
+                                      return stream.mediaAttribute_->label_ == DUMMY_VIDEO_STR;
+                                  });
+    auto count = std::distance(removed, rtpStreams_.end());
+    rtpStreams_.erase(removed, rtpStreams_.end());
+
+    if (count > 0) {
+        JAMI_DBG("[call:%s] Removed %lu dummy video stream(s)", getCallId().c_str(), count);
+        if (count > 1) {
+            JAMI_WARN("[call:%s] Expected to find 1 dummy video stream, found %lu",
+                      getCallId().c_str(),
+                      count);
+        }
+    }
 }
 
 void
diff --git a/src/sip/sipcall.h b/src/sip/sipcall.h
index 7170636b0d..cdcb6c3467 100644
--- a/src/sip/sipcall.h
+++ b/src/sip/sipcall.h
@@ -129,6 +129,8 @@ private:
 public:
     void answer() override;
     void answer(const std::vector<DRing::MediaMap>& mediaList) override;
+    bool checkMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList) override;
+    void handleMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMediaList) override;
     void answerMediaChangeRequest(const std::vector<DRing::MediaMap>& mediaList) override;
     void hangup(int reason) override;
     void refuse() override;
@@ -270,10 +272,12 @@ public:
      * Returns a pointer to the VideoRtp object
      */
     std::shared_ptr<video::VideoRtpSession> getVideoRtp() const;
-    std::shared_ptr<video::VideoRtpSession> addDummyVideoRtpSession();
+    bool addDummyVideoRtpSession() override;
+    void removeDummyVideoRtpSessions() override;
 #endif
     // Get the list of current RTP sessions
     std::vector<std::shared_ptr<RtpSession>> getRtpSessionList() const;
+    static size_t getActiveMediaStreamCount(const std::vector<MediaAttribute>& mediaAttrList);
 
     void setPeerRegisteredName(const std::string& name) { peerRegisteredName_ = name; }
 
-- 
GitLab