From 01594813f594a1029c7b78e0dbca246f372a0df9 Mon Sep 17 00:00:00 2001 From: agsantos <aline.gondimsantos@savoirfairelinux.com> Date: Tue, 26 Oct 2021 11:41:35 -0400 Subject: [PATCH] conference: add handsUp feature GitLab: jami-project#855 Change-Id: I425a4b0049f1ade8c8eeeb93fc79f8d36bd25585 --- bin/dbus/cx.ring.Ring.CallManager.xml | 8 +++ bin/dbus/dbuscallmanager.cpp | 9 ++++ bin/dbus/dbuscallmanager.h | 4 ++ bin/jni/callmanager.i | 1 + bin/nodejs/callmanager.i | 1 + src/client/callmanager.cpp | 9 ++++ src/conference.cpp | 71 +++++++++++++++++++++++++-- src/conference.h | 12 ++++- src/jami/callmanager_interface.h | 4 ++ src/manager.cpp | 16 ++++++ src/manager.h | 3 ++ test/unitTest/call/conference.cpp | 66 +++++++++++++++++++++++++ 12 files changed, 198 insertions(+), 6 deletions(-) diff --git a/bin/dbus/cx.ring.Ring.CallManager.xml b/bin/dbus/cx.ring.Ring.CallManager.xml index a8adc1e659..97ab3b0e6c 100644 --- a/bin/dbus/cx.ring.Ring.CallManager.xml +++ b/bin/dbus/cx.ring.Ring.CallManager.xml @@ -352,6 +352,14 @@ <arg type="b" name="isParticipant" direction="out"/> </method> + <method name="raiseParticipantHand" tp:name-for-bindings="raiseParticipantHand"> + <tp:added version="11.0.0"/> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="confId" direction="in"/> + <arg type="s" name="peerId" direction="in"/> + <arg type="b" name="state" direction="in"/> + </method> + <method name="hangupParticipant" tp:name-for-bindings="hangupParticipant"> <tp:added version="9.8.0"/> <arg type="s" name="confId" direction="in"/> diff --git a/bin/dbus/dbuscallmanager.cpp b/bin/dbus/dbuscallmanager.cpp index 30fe21d08c..dc652ef555 100644 --- a/bin/dbus/dbuscallmanager.cpp +++ b/bin/dbus/dbuscallmanager.cpp @@ -376,4 +376,13 @@ void DBusCallManager::hangupParticipant(const std::string& confId, const std::string& peerId) { DRing::hangupParticipant(confId, peerId); +} + +void +DBusCallManager::raiseParticipantHand(const std::string& accountId, + const std::string& confId, + const std::string& peerId, + const bool& state) +{ + DRing::raiseParticipantHand(accountId, confId, peerId, state); } \ No newline at end of file diff --git a/bin/dbus/dbuscallmanager.h b/bin/dbus/dbuscallmanager.h index 333dbf40a4..f5dd350357 100644 --- a/bin/dbus/dbuscallmanager.h +++ b/bin/dbus/dbuscallmanager.h @@ -118,6 +118,10 @@ public: void setModerator(const std::string& confId, const std::string& peerId, const bool& state); void muteParticipant(const std::string& confId, const std::string& peerId, const bool& state); void hangupParticipant(const std::string& confId, const std::string& peerId); + void raiseParticipantHand(const std::string& accountId, + const std::string& confId, + const std::string& peerId, + const bool& state); }; #endif // __RING_CALLMANAGER_H__ diff --git a/bin/jni/callmanager.i b/bin/jni/callmanager.i index ec00bd1c27..b1ad57fd83 100644 --- a/bin/jni/callmanager.i +++ b/bin/jni/callmanager.i @@ -107,6 +107,7 @@ std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::st void setModerator(const std::string& confId, const std::string& peerId, const bool& state); void muteParticipant(const std::string& confId, const std::string& peerId, const bool& state); void hangupParticipant(const std::string& confId, const std::string& peerId); +void raiseParticipantHand(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); /* File Playback methods */ bool startRecordedFilePlayback(const std::string& filepath); diff --git a/bin/nodejs/callmanager.i b/bin/nodejs/callmanager.i index b7a1b1ee7e..2812acb632 100644 --- a/bin/nodejs/callmanager.i +++ b/bin/nodejs/callmanager.i @@ -96,6 +96,7 @@ std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::st void setModerator(const std::string& confId, const std::string& peerId, const bool& state); void muteParticipant(const std::string& confId, const std::string& peerId, const bool& state); void hangupParticipant(const std::string& confId, const std::string& peerId); +void raiseParticipantHand(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); /* File Playback methods */ bool startRecordedFilePlayback(const std::string& filepath); diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp index cf84f8e98b..c5dc9e1182 100644 --- a/src/client/callmanager.cpp +++ b/src/client/callmanager.cpp @@ -390,6 +390,15 @@ muteParticipant(const std::string& confId, const std::string& peerId, const bool jami::Manager::instance().muteParticipant(confId, peerId, state); } +void +raiseParticipantHand(const std::string& accountId, + const std::string& confId, + const std::string& peerId, + const bool& state) +{ + jami::Manager::instance().raiseParticipantHand(confId, peerId, state); +} + void hangupParticipant(const std::string& confId, const std::string& participant) { diff --git a/src/conference.cpp b/src/conference.cpp index 5206471576..c4a4a75129 100644 --- a/src/conference.cpp +++ b/src/conference.cpp @@ -105,6 +105,7 @@ Conference::Conference(bool enableVideo) 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), @@ -118,7 +119,8 @@ Conference::Conference(bool enableVideo) !info.hasVideo, isLocalMuted, isModeratorMuted, - isModerator}); + isModerator, + isHandRaised}); } if (auto videoMixer = shared->getVideoMixer()) { newInfo.h = videoMixer->getHeight(); @@ -127,8 +129,11 @@ Conference::Conference(bool enableVideo) lk.unlock(); if (!hostAdded) { auto audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); - newInfo.emplace_back(ParticipantInfo { - "", "", "", false, 0, 0, 0, 0, true, audioLocalMuted, false, true}); + ParticipantInfo pi; + pi.videoMuted = true; + pi.audioLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); + pi.isModerator = true; + newInfo.emplace_back(pi); } shared->updateConferenceInfo(std::move(newInfo)); @@ -433,7 +438,6 @@ Conference::addParticipant(const std::string& participant_id) void Conference::setActiveParticipant(const std::string& participant_id) { - // TODO. Shouldn't be protected by ENABLE_VIDEO define ? if (!videoMixer_) return; if (isHost(participant_id)) { @@ -570,6 +574,7 @@ Conference::removeParticipant(const std::string& participant_id) if (participants_.erase(participant_id)) { 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()) @@ -871,6 +876,10 @@ Conference::onConfOrder(const std::string& callId, const std::string& confOrder) if (root.isMember("hangupParticipant")) { hangupParticipant(root["hangupParticipant"].asString()); } + if (root.isMember("handRaised")) { + setHandRaised(root["handRaised"].asString(), + root["handState"].asString() == "true"); + } } } @@ -886,6 +895,50 @@ 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 : participants_) { + 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) { @@ -919,6 +972,16 @@ Conference::updateModerators() sendConferenceInfos(); } +void +Conference::updateHandsRaised() +{ + std::lock_guard<std::mutex> lk(confInfoMutex_); + for (auto& info : confInfo_) { + info.handRaised = isHandRaised(string_remove_suffix(info.uri, '@')); + } + sendConferenceInfos(); +} + bool Conference::isMuted(std::string_view uri) const { diff --git a/src/conference.h b/src/conference.h index 143319f1cf..424fa4e638 100644 --- a/src/conference.h +++ b/src/conference.h @@ -70,6 +70,7 @@ struct ParticipantInfo bool audioLocalMuted {false}; bool audioModeratorMuted {false}; bool isModerator {false}; + bool handRaised {false}; void fromJson(const Json::Value& v) { @@ -85,6 +86,7 @@ struct ParticipantInfo audioLocalMuted = v["audioLocalMuted"].asBool(); audioModeratorMuted = v["audioModeratorMuted"].asBool(); isModerator = v["isModerator"].asBool(); + handRaised = v["handRaised"].asBool(); } Json::Value toJson() const @@ -102,6 +104,7 @@ struct ParticipantInfo val["audioLocalMuted"] = audioLocalMuted; val["audioModeratorMuted"] = audioModeratorMuted; val["isModerator"] = isModerator; + val["handRaised"] = handRaised; return val; } @@ -118,7 +121,8 @@ struct ParticipantInfo {"videoMuted", videoMuted ? "true" : "false"}, {"audioLocalMuted", audioLocalMuted ? "true" : "false"}, {"audioModeratorMuted", audioModeratorMuted ? "true" : "false"}, - {"isModerator", isModerator ? "true" : "false"}}; + {"isModerator", isModerator ? "true" : "false"}, + {"handRaised", handRaised ? "true" : "false"}}; } friend bool operator==(const ParticipantInfo& p1, const ParticipantInfo& p2) @@ -128,7 +132,7 @@ struct ParticipantInfo and p1.h == p2.h and p1.videoMuted == p2.videoMuted and p1.audioLocalMuted == p2.audioLocalMuted and p1.audioModeratorMuted == p2.audioModeratorMuted - and p1.isModerator == p2.isModerator; + and p1.isModerator == p2.isModerator and p1.handRaised == p2.handRaised; } friend bool operator!=(const ParticipantInfo& p1, const ParticipantInfo& p2) @@ -320,6 +324,7 @@ public: void updateConferenceInfo(ConfInfo confInfo); void createSinks(const ConfInfo& infos); void setModerator(const std::string& uri, const bool& state); + void setHandRaised(const std::string& uri, const bool& state); void muteParticipant(const std::string& uri, const bool& state); void hangupParticipant(const std::string& participant_id); void updateMuted(); @@ -335,7 +340,9 @@ private: static std::shared_ptr<Call> getCall(const std::string& callId); bool isModerator(std::string_view uri) const; + bool isHandRaised(std::string_view uri) const; void updateModerators(); + void updateHandsRaised(); std::string id_; State confState_ {State::ACTIVE_ATTACHED}; @@ -361,6 +368,7 @@ private: std::shared_ptr<jami::AudioInput> audioMixer_; std::set<std::string, std::less<>> moderators_ {}; std::set<std::string, std::less<>> participantsMuted_ {}; + std::set<std::string, std::less<>> handsRaised_; void initRecorder(std::shared_ptr<MediaRecorder>& rec); void deinitRecorder(std::shared_ptr<MediaRecorder>& rec); diff --git a/src/jami/callmanager_interface.h b/src/jami/callmanager_interface.h index 944b4d43be..0c3fef7b00 100644 --- a/src/jami/callmanager_interface.h +++ b/src/jami/callmanager_interface.h @@ -97,6 +97,10 @@ DRING_PUBLIC void setModerator(const std::string& confId, DRING_PUBLIC void muteParticipant(const std::string& confId, const std::string& peerId, const bool& state); +DRING_PUBLIC void raiseParticipantHand(const std::string& accountId, + const std::string& confId, + const std::string& peerId, + const bool& state); DRING_PUBLIC void hangupParticipant(const std::string& confId, const std::string& participant); /* Statistic related methods */ diff --git a/src/manager.cpp b/src/manager.cpp index 305cc12925..fd8dc23b0a 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -3583,6 +3583,22 @@ Manager::muteParticipant(const std::string& confId, } } +void +Manager::raiseParticipantHand(const std::string& confId, + const std::string& participant, + const bool& state) +{ + if (auto conf = getConferenceFromID(confId)) { + conf->setHandRaised(participant, state); + } else if (auto call = getCallFromCallID(confId)) { + std::map<std::string, std::string> messages; + Json::Value root; + root["handRaised"] = participant; + root["handState"] = state ? TRUE_STR : FALSE_STR; + call->sendConfOrder(root); + } +} + void Manager::setDefaultModerator(const std::string& accountID, const std::string& peerURI, bool state) { diff --git a/src/manager.h b/src/manager.h index 5accbd5461..464d8ba711 100644 --- a/src/manager.h +++ b/src/manager.h @@ -1023,6 +1023,9 @@ public: void setModerator(const std::string& confId, const std::string& peerId, const bool& state); void muteParticipant(const std::string& confId, const std::string& peerId, const bool& state); void hangupParticipant(const std::string& confId, const std::string& participant); + void raiseParticipantHand(const std::string& confId, + const std::string& participant, + const bool& state); void setDefaultModerator(const std::string& accountID, const std::string& peerURI, bool state); std::vector<std::string> getDefaultModerators(const std::string& accountID); diff --git a/test/unitTest/call/conference.cpp b/test/unitTest/call/conference.cpp index 1b12ad9e6b..71bfd9f9ef 100644 --- a/test/unitTest/call/conference.cpp +++ b/test/unitTest/call/conference.cpp @@ -43,12 +43,14 @@ struct CallData std::string callId {}; std::string state {}; std::atomic_bool moderatorMuted {false}; + std::atomic_bool raisedHand {false}; void reset() { callId = ""; state = ""; moderatorMuted = false; + raisedHand = false; } }; @@ -73,6 +75,7 @@ private: void testAudioVideoMutedStates(); void testCreateParticipantsSinks(); void testMuteStatusAfterRemove(); + void testHandsUp(); CPPUNIT_TEST_SUITE(ConferenceTest); CPPUNIT_TEST(testGetConference); @@ -80,6 +83,7 @@ private: CPPUNIT_TEST(testAudioVideoMutedStates); CPPUNIT_TEST(testCreateParticipantsSinks); CPPUNIT_TEST(testMuteStatusAfterRemove); + CPPUNIT_TEST(testHandsUp); CPPUNIT_TEST_SUITE_END(); // Common parts @@ -179,10 +183,13 @@ ConferenceTest::registerSignalHandlers() for (const auto& infos : participantsInfos) { if (infos.at("uri").find(bobUri) != std::string::npos) { bobCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; + bobCall.raisedHand = infos.at("handRaised") == "true"; } else if (infos.at("uri").find(carlaUri) != std::string::npos) { carlaCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; + carlaCall.raisedHand = infos.at("handRaised") == "true"; } else if (infos.at("uri").find(daviUri) != std::string::npos) { daviCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; + daviCall.raisedHand = infos.at("handRaised") == "true"; } } cv.notify_one(); @@ -405,6 +412,65 @@ ConferenceTest::testMuteStatusAfterRemove() DRing::unregisterSignalHandlers(); } + +void +ConferenceTest::testHandsUp() +{ + registerSignalHandlers(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto daviAccount = Manager::instance().getAccount<JamiAccount>(daviId); + auto daviUri = daviAccount->getUsername(); + + startConference(); + + JAMI_INFO("Play with raise hand"); + Manager::instance().raiseParticipantHand(confId, bobUri, true); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(5), [&] { return bobCall.raisedHand.load(); })); + + Manager::instance().raiseParticipantHand(confId, bobUri, false); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(5), [&] { return !bobCall.raisedHand.load(); })); + + JAMI_INFO("Start call between Alice and Davi"); + auto call1 = aliceAccount->newOutgoingCall(daviUri); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + Manager::instance().answerCall(daviCall.callId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "CURRENT"; })); + Manager::instance().addParticipant(daviCall.callId, confId); + + Manager::instance().raiseParticipantHand(confId, daviUri, true); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(5), [&] { return daviCall.raisedHand.load(); })); + + Manager::instance().hangupCall(daviCall.callId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + daviCall.reset(); + + auto call2 = aliceAccount->newOutgoingCall(daviUri); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + Manager::instance().answerCall(daviCall.callId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "CURRENT"; })); + Manager::instance().addParticipant(daviCall.callId, confId); + + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(5), [&] { return !daviCall.raisedHand.load(); })); + + Manager::instance().hangupCall(daviCall.callId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + hangupConference(); + + DRing::unregisterSignalHandlers(); +} } // namespace test } // namespace jami -- GitLab