From 073d68f9a062fa366d78fcd95f20e49093e18080 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 28 Sep 2020 16:27:19 -0400
Subject: [PATCH] conference: a conference can now be controlled from
 authorized accounts

Implement application+confOrder to allow a remote peer to control the
conference's layout. The authorized peer can send a json likes:
{
    "layout": 1,
    "activeParticipant: "uri"
}
For now authorized peers are local accounts. This also change the conference
layout by sending "isMaster" for each participants to allow the peer to know
if we are the master when we are not the host.

Change-Id: Iba8a55453421e3f173b43e773d1b43502efec747
Gitlab: #241
---
 src/call.cpp       |   7 +++
 src/conference.cpp | 108 +++++++++++++++++++++++++++++++++++++++++----
 src/conference.h   |  13 +++++-
 src/manager.cpp    |  39 +++++++++-------
 4 files changed, 140 insertions(+), 27 deletions(-)

diff --git a/src/call.cpp b/src/call.cpp
index db8736fcd6..919e6f830a 100644
--- a/src/call.cpp
+++ b/src/call.cpp
@@ -419,6 +419,13 @@ Call::onTextMessage(std::map<std::string, std::string>&& messages)
         return;
     }
 
+    it = messages.find("application/confOrder+json");
+    if (it != messages.end()) {
+        if (auto conf = Manager::instance().getConferenceFromID(confID_))
+            conf->onConfOrder(getCallId(), it->second);
+        return;
+    }
+
     {
         std::lock_guard<std::recursive_mutex> lk {callMutex_};
         if (parent_) {
diff --git a/src/conference.cpp b/src/conference.cpp
index 03e7f2cc14..6bf29a15f7 100644
--- a/src/conference.cpp
+++ b/src/conference.cpp
@@ -19,12 +19,14 @@
  *  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 "audio/ringbufferpool.h"
+#include "jamidht/jamiaccount.h"
 
 #ifdef ENABLE_VIDEO
 #include "sip/sipcall.h"
@@ -47,6 +49,17 @@ Conference::Conference()
 {
     JAMI_INFO("Create new conference %s", id_.c_str());
 
+    // TODO: For now, add all accounts on the same device as
+    // conference master. In the future, this should be
+    // retrieven with another way
+    auto accounts = jami::Manager::instance().getAllAccounts<JamiAccount>();
+    moderators_.reserve(accounts.size());
+    for (const auto& account : accounts) {
+        if (!account)
+            continue;
+        moderators_.emplace_back(account->getUsername());
+    }
+
 #ifdef ENABLE_VIDEO
     getVideoMixer()->setOnSourcesUpdated([this](const std::vector<video::SourceInfo>&& infos) {
         runOnMainThread([w = weak(), infos = std::move(infos)] {
@@ -79,8 +92,15 @@ Conference::Conference()
                                  and not videoMixer->getActiveParticipant()); // by default, local
                                                                               // is shown as active
                 subCalls.erase(it->second);
-                newInfo.emplace_back(ParticipantInfo {
-                    std::move(uri), active, info.x, info.y, info.w, info.h, !info.hasVideo, false});
+                newInfo.emplace_back(ParticipantInfo {std::move(uri),
+                                                      active,
+                                                      info.x,
+                                                      info.y,
+                                                      info.w,
+                                                      info.h,
+                                                      !info.hasVideo,
+                                                      false,
+                                                      shared->isModerator(uri)});
             }
             lk.unlock();
             // Handle participants not present in the video mixer
@@ -88,7 +108,15 @@ Conference::Conference()
                 std::string uri = "";
                 if (auto call = Manager::instance().callFactory.getCall<SIPCall>(subCall))
                     uri = call->getPeerNumber();
-                ParticipantInfo {std::move(uri), false, 0, 0, 0, 0, true, false};
+                ParticipantInfo {std::move(uri),
+                                 false,
+                                 0,
+                                 0,
+                                 0,
+                                 0,
+                                 true,
+                                 false,
+                                 shared->isModerator(uri)};
             }
 
             {
@@ -119,7 +147,8 @@ Conference::~Conference()
                 JAMI_DBG("Stop recording for conf %s", getConfID().c_str());
                 this->toggleRecording();
                 if (not call->isRecording()) {
-                    JAMI_DBG("Conference was recorded, start recording for conf %s", call->getCallId().c_str());
+                    JAMI_DBG("Conference was recorded, start recording for conf %s",
+                             call->getCallId().c_str());
                     call->toggleRecording();
                 }
             }
@@ -155,12 +184,12 @@ Conference::add(const std::string& participant_id)
                 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());
+                    JAMI_DBG("One participant was recording, start recording for conference %s",
+                             getConfID().c_str());
                     this->toggleRecording();
                 }
             }
-        }
-        else
+        } else
             JAMI_ERR("no call associate to participant %s", participant_id.c_str());
 #endif // ENABLE_VIDEO
     }
@@ -172,8 +201,9 @@ Conference::setActiveParticipant(const std::string& participant_id)
     if (!videoMixer_)
         return;
     for (const auto& item : participants_) {
-        if (participant_id == item) {
-            if (auto call = Manager::instance().callFactory.getCall<SIPCall>(participant_id)) {
+        if (auto call = Manager::instance().callFactory.getCall<SIPCall>(item)) {
+            if (participant_id == item
+                || call->getPeerNumber().find(participant_id) != std::string::npos) {
                 videoMixer_->setActiveParticipant(call->getVideoRtp().getVideoReceive().get());
                 return;
             }
@@ -183,6 +213,24 @@ Conference::setActiveParticipant(const std::string& participant_id)
     videoMixer_->setActiveParticipant(nullptr);
 }
 
+void
+Conference::setLayout(int layout)
+{
+    switch (layout) {
+    case 0:
+        getVideoMixer()->setVideoLayout(video::Layout::GRID);
+        break;
+    case 1:
+        getVideoMixer()->setVideoLayout(video::Layout::ONE_BIG_WITH_SMALL);
+        break;
+    case 2:
+        getVideoMixer()->setVideoLayout(video::Layout::ONE_BIG);
+        break;
+    default:
+        break;
+    }
+}
+
 std::vector<std::map<std::string, std::string>>
 ConfInfo::toVectorMapStringString() const
 {
@@ -434,4 +482,46 @@ Conference::deinitRecorder(std::shared_ptr<MediaRecorder>& rec)
     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 uri = call->getPeerNumber();
+        auto separator = uri.find('@');
+        if (separator != std::string::npos)
+            uri = uri.substr(0, separator - 1);
+        if (!isModerator(uri)) {
+            JAMI_WARN("Received conference order from a non master (%s)", uri.c_str());
+            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", uri.c_str());
+            return;
+        }
+        if (root.isMember("layout")) {
+            setLayout(root["layout"].asUInt());
+        }
+        if (root.isMember("activeParticipant")) {
+            setActiveParticipant(root["activeParticipant"].asString());
+        }
+    }
+}
+
+bool
+Conference::isModerator(const std::string& uri) const
+{
+    return std::find_if(moderators_.begin(),
+                        moderators_.end(),
+                        [&uri](const std::string& master) {
+                            return master.find(uri) != std::string::npos;
+                        })
+           != moderators_.end();
+}
+
 } // namespace jami
diff --git a/src/conference.h b/src/conference.h
index 48955e1175..709274b236 100644
--- a/src/conference.h
+++ b/src/conference.h
@@ -31,7 +31,6 @@
 
 #include "audio/audio_input.h"
 
-
 #include <json/json.h>
 
 #include "recordable.h"
@@ -54,6 +53,7 @@ struct ParticipantInfo
     int h {0};
     bool videoMuted {false};
     bool audioMuted {false};
+    bool isModerator {false};
 
     void fromJson(const Json::Value& v)
     {
@@ -65,6 +65,7 @@ struct ParticipantInfo
         h = v["h"].asInt();
         videoMuted = v["videoMuted"].asBool();
         audioMuted = v["audioMuted"].asBool();
+        isModerator = v["isModerator"].asBool();
     }
 
     Json::Value toJson() const
@@ -78,6 +79,7 @@ struct ParticipantInfo
         val["h"] = h;
         val["videoMuted"] = videoMuted;
         val["audioMuted"] = audioMuted;
+        val["isModerator"] = isModerator;
         return val;
     }
 
@@ -90,7 +92,8 @@ struct ParticipantInfo
                 {"w", std::to_string(w)},
                 {"h", std::to_string(h)},
                 {"videoMuted", videoMuted ? "true" : "false"},
-                {"audioMuted", audioMuted ? "true" : "false"}};
+                {"audioMuted", audioMuted ? "true" : "false"},
+                {"isModerator", isModerator ? "true" : "false"}};
     }
 };
 
@@ -193,10 +196,13 @@ public:
     void switchInput(const std::string& input);
 
     void setActiveParticipant(const std::string& participant_id);
+    void setLayout(int layout);
 
     void attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame, const std::string& callId);
     void detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame);
 
+    void onConfOrder(const std::string& callId, const std::string& order);
+
 #ifdef ENABLE_VIDEO
     std::shared_ptr<video::VideoMixer> getVideoMixer();
     std::string getVideoInput() const { return mediaInput_; }
@@ -214,6 +220,8 @@ private:
         return std::static_pointer_cast<Conference>(shared_from_this());
     }
 
+    bool isModerator(const std::string& uri) const;
+
     std::string id_;
     State confState_ {State::ACTIVE_ATTACHED};
     ParticipantSet participants_;
@@ -232,6 +240,7 @@ private:
 #endif
 
     std::shared_ptr<jami::AudioInput> audioMixer_;
+    std::vector<std::string> moderators_ {};
 
     void initRecorder(std::shared_ptr<MediaRecorder>& rec);
     void deinitRecorder(std::shared_ptr<MediaRecorder>& rec);
diff --git a/src/manager.cpp b/src/manager.cpp
index e1edd87eee..cb6f8cd100 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -1469,28 +1469,35 @@ void
 Manager::setConferenceLayout(const std::string& confId, int layout)
 {
     if (auto conf = getConferenceFromID(confId)) {
-        auto videoMixer = conf->getVideoMixer();
-        switch (layout) {
-        case 0:
-            videoMixer->setVideoLayout(video::Layout::GRID);
-            break;
-        case 1:
-            videoMixer->setVideoLayout(video::Layout::ONE_BIG_WITH_SMALL);
-            break;
-        case 2:
-            videoMixer->setVideoLayout(video::Layout::ONE_BIG);
-            break;
-        default:
-            break;
-        }
+        conf->setLayout(layout);
+    } else if (auto call = getCallFromCallID(confId)) {
+        std::map<std::string, std::string> messages;
+        Json::Value root;
+        root["layout"] = layout;
+        Json::StreamWriterBuilder wbuilder;
+        wbuilder["commentStyle"] = "None";
+        wbuilder["indentation"] = "";
+        auto output = Json::writeString(wbuilder, root);
+        messages["application/confOrder+json"] = output;
+        call->sendTextMessage(messages, call->getPeerDisplayName());
     }
 }
 
 void
-Manager::setActiveParticipant(const std::string& confId, const std::string& callId)
+Manager::setActiveParticipant(const std::string& confId, const std::string& participant)
 {
     if (auto conf = getConferenceFromID(confId)) {
-        conf->setActiveParticipant(callId);
+        conf->setActiveParticipant(participant);
+    } else if (auto call = getCallFromCallID(confId)) {
+        std::map<std::string, std::string> messages;
+        Json::Value root;
+        root["activeParticipant"] = participant;
+        Json::StreamWriterBuilder wbuilder;
+        wbuilder["commentStyle"] = "None";
+        wbuilder["indentation"] = "";
+        auto output = Json::writeString(wbuilder, root);
+        messages["application/confOrder+json"] = output;
+        call->sendTextMessage(messages, call->getPeerDisplayName());
     }
 }
 
-- 
GitLab