From ea2e2e4dbc4f788e69a121b9a1c4b333802d6798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= Date: Thu, 14 Apr 2022 10:31:34 -0400 Subject: [PATCH] conference: improve conference protocol for multi stream The current design got some limitations. It's not possible to control informations per shared media. This means that we can't got several active sinks for example. The goal of this patch here is to update the conferences orders to be able to control the state of each sink individually and for the client to be able to handle conferences with accounts connected via several devices with several medias per devices. So each orders is sent with a different level (account/device/media) For example, we will be able to send a moderator order for an account, to hangup a device or to set a media active. To support those orders, both sides MUST be patched. Else, the old protocol will be used. The version of the protocol supported is sent in the conferences informations to notify the peer what version to use. Finally, this patch changes some APIs to support multisteam: + Some APIs now takes the deviceId or the sinkId when necessary https://git.jami.net/savoirfairelinux/jami-project/-/issues/1429 Doc: https://git.jami.net/savoirfairelinux/jami-project/-/wikis/technical/6.1.-Conference-Protocol Change-Id: Ieedd6055fd43b2a09b2cc8b253dcd6a3bf260a39 --- bin/dbus/cx.ring.Ring.CallManager.xml | 30 ++ bin/dbus/dbuscallmanager.cpp | 37 ++- bin/dbus/dbuscallmanager.h | 25 +- bin/jni/callmanager.i | 23 +- bin/nodejs/callmanager.i | 23 +- configure.ac | 2 +- meson.build | 2 +- src/CMakeLists.txt | 2 + src/Makefile.am | 2 + src/call.cpp | 4 + src/call.h | 5 + src/client/callmanager.cpp | 178 +++++++++-- src/conference.cpp | 424 ++++++++++++++++---------- src/conference.h | 35 ++- src/conference_protocol.cpp | 176 +++++++++++ src/conference_protocol.h | 141 +++++++++ src/jami/callmanager_interface.h | 34 ++- src/media/video/video_mixer.cpp | 30 +- src/media/video/video_mixer.h | 28 +- src/media/video/video_rtp_session.cpp | 8 +- src/meson.build | 1 + src/sip/sipcall.cpp | 3 + test/unitTest/call/conference.cpp | 282 ++++++++++------- 23 files changed, 1139 insertions(+), 356 deletions(-) create mode 100644 src/conference_protocol.cpp create mode 100644 src/conference_protocol.h diff --git a/bin/dbus/cx.ring.Ring.CallManager.xml b/bin/dbus/cx.ring.Ring.CallManager.xml index e1a9130ba..6f724bd76 100644 --- a/bin/dbus/cx.ring.Ring.CallManager.xml +++ b/bin/dbus/cx.ring.Ring.CallManager.xml @@ -334,6 +334,16 @@ + + + + + + + + + + @@ -350,6 +360,16 @@ + + + + + + + + + + @@ -365,11 +385,21 @@ + + + + + + + + + + diff --git a/bin/dbus/dbuscallmanager.cpp b/bin/dbus/dbuscallmanager.cpp index 2501a9391..a5b55ebca 100644 --- a/bin/dbus/dbuscallmanager.cpp +++ b/bin/dbus/dbuscallmanager.cpp @@ -183,6 +183,38 @@ DBusCallManager::setActiveParticipant(const std::string& accountId, DRing::setActiveParticipant(accountId, confId, callId); } +void +DBusCallManager::muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state) +{ + DRing::muteStream(accountId, confId, accountUri, deviceId, streamId, state); +} + +void +DBusCallManager::setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state) +{ + DRing::setActiveStream(accountId, confId, accountUri, deviceId, streamId, state); +} + +void +DBusCallManager::raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state) +{ + DRing::raiseHand(accountId, confId, accountUri, deviceId, state); +} + auto DBusCallManager::isConferenceParticipant(const std::string& accountId, const std::string& call_id) -> decltype(DRing::isConferenceParticipant(accountId, call_id)) @@ -388,9 +420,10 @@ DBusCallManager::muteParticipant(const std::string& accountId, void DBusCallManager::hangupParticipant(const std::string& accountId, const std::string& confId, - const std::string& peerId) + const std::string& peerId, + const std::string& deviceId) { - DRing::hangupParticipant(accountId, confId, peerId); + DRing::hangupParticipant(accountId, confId, peerId, deviceId); } void diff --git a/bin/dbus/dbuscallmanager.h b/bin/dbus/dbuscallmanager.h index 76aaaa405..572d6251a 100644 --- a/bin/dbus/dbuscallmanager.h +++ b/bin/dbus/dbuscallmanager.h @@ -144,13 +144,32 @@ public: const std::string& confId, const std::string& peerId, const bool& state); + void muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); + void setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); + void raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state); + void hangupParticipant(const std::string& accountId, + const std::string& confId, + const std::string& peerId, + const std::string& deviceId); + // DEPRECATED void muteParticipant(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); - void hangupParticipant(const std::string& accountId, - const std::string& confId, - const std::string& peerId); void raiseParticipantHand(const std::string& accountId, const std::string& confId, const std::string& peerId, diff --git a/bin/jni/callmanager.i b/bin/jni/callmanager.i index 6de910416..8ff6ff528 100644 --- a/bin/jni/callmanager.i +++ b/bin/jni/callmanager.i @@ -102,8 +102,29 @@ std::string getConferenceId(const std::string& accountId, const std::string& cal std::map getConferenceDetails(const std::string& accountId, const std::string& callId); std::vector> getConferenceInfos(const std::string& accountId, const std::string& confId); void setModerator(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); +void muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); +void setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); +void hangupParticipant(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId); +void raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state); +// DEPRECATED void muteParticipant(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); -void hangupParticipant(const std::string& accountId, 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 */ diff --git a/bin/nodejs/callmanager.i b/bin/nodejs/callmanager.i index 6de910416..8ff6ff528 100644 --- a/bin/nodejs/callmanager.i +++ b/bin/nodejs/callmanager.i @@ -102,8 +102,29 @@ std::string getConferenceId(const std::string& accountId, const std::string& cal std::map getConferenceDetails(const std::string& accountId, const std::string& callId); std::vector> getConferenceInfos(const std::string& accountId, const std::string& confId); void setModerator(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); +void muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); +void setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); +void hangupParticipant(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId); +void raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state); +// DEPRECATED void muteParticipant(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); -void hangupParticipant(const std::string& accountId, 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 */ diff --git a/configure.ac b/configure.ac index f44f6ac81..6be137048 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl Jami - configure.ac dnl Process this file with autoconf to produce a configure script. AC_PREREQ([2.69]) -AC_INIT([Jami Daemon],[13.0.0],[jami@gnu.org],[jami]) +AC_INIT([Jami Daemon],[13.1.0],[jami@gnu.org],[jami]) dnl Clear the implicit flags that default to '-g -O2', otherwise they dnl take precedence over the values we set via the diff --git a/meson.build b/meson.build index de2282be5..f62198119 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jami-daemon', ['c', 'cpp'], - version: '13.0.0', + version: '13.1.0', license: 'GPL3+', default_options: ['cpp_std=gnu++17', 'buildtype=debugoptimized'], meson_version:'>= 0.56' diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e5957f90b..78cf16a5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,8 @@ list (APPEND Source_Files "${CMAKE_CURRENT_SOURCE_DIR}/compiler_intrinsics.h" "${CMAKE_CURRENT_SOURCE_DIR}/conference.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/conference.h" + "${CMAKE_CURRENT_SOURCE_DIR}/conference_protocol.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/conference_protocol.h" "${CMAKE_CURRENT_SOURCE_DIR}/data_transfer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/data_transfer.h" "${CMAKE_CURRENT_SOURCE_DIR}/enumclass_utils.h" diff --git a/src/Makefile.am b/src/Makefile.am index 64195d163..09ab4f5b7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -75,6 +75,7 @@ endif libring_la_SOURCES = \ buildinfo.cpp \ conference.cpp \ + conference_protocol.cpp \ account_factory.cpp \ call_factory.cpp \ preferences.cpp \ @@ -93,6 +94,7 @@ libring_la_SOURCES = \ ice_transport.h \ threadloop.h \ conference.h \ + conference_protocol.h \ account_factory.h \ call_factory.h \ preferences.h \ diff --git a/src/call.cpp b/src/call.cpp index f1df75990..4fba51b2f 100644 --- a/src/call.cpp +++ b/src/call.cpp @@ -655,6 +655,10 @@ Call::setConferenceInfo(const std::string& msg) newInfo.emplace_back(pInfo); } } + if (json.isMember("v")) { + newInfo.v = json["v"].asInt(); + peerConfProtocol_ = newInfo.v; + } if (json.isMember("w")) newInfo.w = json["w"].asInt(); if (json.isMember("h")) diff --git a/src/call.h b/src/call.h index 55caa1430..7b7f05000 100644 --- a/src/call.h +++ b/src/call.h @@ -436,6 +436,8 @@ public: virtual void monitor() const = 0; + int conferenceProtocolVersion() const { return peerConfProtocol_; } + protected: using clock = std::chrono::steady_clock; using time_point = clock::time_point; @@ -516,6 +518,9 @@ protected: ///< MultiDevice: message received by subcall to merged yet MsgList pendingInMessages_; + + /// Supported conference protocol version + int peerConfProtocol_ {0}; }; // Helpers diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp index ed257f67f..1f9771c6a 100644 --- a/src/client/callmanager.cpp +++ b/src/client/callmanager.cpp @@ -27,12 +27,14 @@ #include "client/ring_signal.h" #include "sip/sipvoiplink.h" +#include "sip/sipcall.h" #include "audio/audiolayer.h" #include "media/media_attribute.h" #include "string_utils.h" #include "logger.h" #include "manager.h" +#include "jamidht/jamiaccount.h" #include "smartools.h" @@ -211,22 +213,6 @@ setConferenceLayout(const std::string& accountId, const std::string& confId, uin } } -void -setActiveParticipant(const std::string& accountId, - const std::string& confId, - const std::string& participant) -{ - if (const auto account = jami::Manager::instance().getAccount(accountId)) { - if (auto conf = account->getConference(confId)) { - conf->setActiveParticipant(participant); - } else if (auto call = account->getCall(confId)) { - Json::Value root; - root["activeParticipant"] = participant; - call->sendConfOrder(root); - } - } -} - bool isConferenceParticipant(const std::string& accountId, const std::string& callId) { @@ -270,7 +256,7 @@ detachLocalParticipant() } bool -detachParticipant(const std::string& accountId, const std::string& callId) +detachParticipant(const std::string&, const std::string& callId) { return jami::Manager::instance().detachParticipant(callId); } @@ -514,6 +500,7 @@ muteParticipant(const std::string& accountId, const std::string& peerId, const bool& state) { + JAMI_ERR() << "muteParticipant is deprecated, please use muteStream"; if (const auto account = jami::Manager::instance().getAccount(accountId)) { if (auto conf = account->getConference(confId)) { conf->muteParticipant(peerId, state); @@ -527,27 +514,139 @@ muteParticipant(const std::string& accountId, } void -hangupParticipant(const std::string& accountId, - const std::string& confId, - const std::string& participant) +muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state) { if (const auto account = jami::Manager::instance().getAccount(accountId)) { if (auto conf = account->getConference(confId)) { - conf->hangupParticipant(participant); + conf->muteStream(accountUri, deviceId, streamId, state); + } else if (auto call = account->getCall(confId)) { + if (call->conferenceProtocolVersion() == 1) { + Json::Value sinkVal; + sinkVal["muteAudio"] = state; + Json::Value mediasObj; + mediasObj[streamId] = sinkVal; + Json::Value deviceVal; + deviceVal["medias"] = mediasObj; + Json::Value deviceObj; + deviceObj[deviceId] = deviceVal; + Json::Value accountVal; + deviceVal["devices"] = deviceObj; + Json::Value root; + root[accountUri] = deviceVal; + root["version"] = 1; + call->sendConfOrder(root); + } else if (call->conferenceProtocolVersion() == 0) { + Json::Value root; + root["muteParticipant"] = accountUri; + root["muteState"] = state ? jami::TRUE_STR : jami::FALSE_STR; + call->sendConfOrder(root); + } + } + } +} + +void +setActiveParticipant(const std::string& accountId, + const std::string& confId, + const std::string& participant) +{ + JAMI_ERR() << "setActiveParticipant is deprecated, please use setActiveStream"; + if (const auto account = jami::Manager::instance().getAccount(accountId)) { + if (auto conf = account->getConference(confId)) { + conf->setActiveParticipant(participant); } else if (auto call = account->getCall(confId)) { Json::Value root; - root["hangupParticipant"] = participant; + root["activeParticipant"] = participant; call->sendConfOrder(root); } } } +void +setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state) +{ + if (const auto account = jami::Manager::instance().getAccount(accountId)) { + if (auto conf = account->getConference(confId)) { + // TODO, as for now the videoMixer doesn't have the streamId + if (deviceId == account->currentDeviceId() && accountUri == account->getUsername()) { + conf->setActiveStream("", state); + } else if (auto call = std::static_pointer_cast( + conf->getCallFromPeerID(accountUri))) { + conf->setActiveStream(call->getCallId(), state); + } + // conf->setActiveStream(streamId, state); + } else if (auto call = account->getCall(confId)) { + if (call->conferenceProtocolVersion() == 1) { + Json::Value sinkVal; + sinkVal["active"] = state; + Json::Value mediasObj; + mediasObj[streamId] = sinkVal; + Json::Value deviceVal; + deviceVal["medias"] = mediasObj; + Json::Value deviceObj; + deviceObj[deviceId] = deviceVal; + Json::Value accountVal; + deviceVal["devices"] = deviceObj; + Json::Value root; + root[accountUri] = deviceVal; + root["version"] = 1; + call->sendConfOrder(root); + } else if (call->conferenceProtocolVersion() == 0) { + Json::Value root; + root["activeParticipant"] = accountUri; + call->sendConfOrder(root); + } + } + } +} + +void +hangupParticipant(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId) +{ + if (const auto account = jami::Manager::instance().getAccount(accountId)) { + if (auto conf = account->getConference(confId)) { + conf->hangupParticipant(accountUri, deviceId); + } else if (auto call = std::static_pointer_cast(account->getCall(confId))) { + if (call->conferenceProtocolVersion() == 1) { + Json::Value deviceVal; + deviceVal["hangup"] = jami::TRUE_STR; + Json::Value deviceObj; + deviceObj[deviceId] = deviceVal; + Json::Value accountVal; + deviceVal["devices"] = deviceObj; + Json::Value root; + root[accountUri] = deviceVal; + root["version"] = 1; + call->sendConfOrder(root); + } else if (call->conferenceProtocolVersion() == 0) { + Json::Value root; + root["hangupParticipant"] = accountUri; + call->sendConfOrder(root); + } + } + } +} + void raiseParticipantHand(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state) { + JAMI_ERR() << "raiseParticipantHand is deprecated, please use raiseHand"; if (const auto account = jami::Manager::instance().getAccount(accountId)) { if (auto conf = account->getConference(confId)) { conf->setHandRaised(peerId, state); @@ -560,4 +659,39 @@ raiseParticipantHand(const std::string& accountId, } } +void +raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state) +{ + if (const auto account = jami::Manager::instance().getAccount(accountId)) { + if (auto conf = account->getConference(confId)) { + conf->setHandRaised(accountUri, state); + } else if (auto call = std::static_pointer_cast(account->getCall(confId))) { + if (call->conferenceProtocolVersion() == 1) { + Json::Value deviceVal; + deviceVal["raiseHand"] = state; + Json::Value deviceObj; + std::string device = deviceId.empty() ? std::string(account->currentDeviceId()) + : deviceId; + deviceObj[device] = deviceVal; + Json::Value accountVal; + deviceVal["devices"] = deviceObj; + Json::Value root; + std::string uri = accountUri.empty() ? account->getUsername() : accountUri; + root[uri] = deviceVal; + root["version"] = 1; + call->sendConfOrder(root); + } else if (call->conferenceProtocolVersion() == 0) { + Json::Value root; + root["handRaised"] = account->getUsername(); + root["handState"] = state ? jami::TRUE_STR : jami::FALSE_STR; + call->sendConfOrder(root); + } + } + } +} + } // namespace DRing diff --git a/src/conference.cpp b/src/conference.cpp index 9ea5af27e..f4f69e1c8 100644 --- a/src/conference.cpp +++ b/src/conference.cpp @@ -101,6 +101,12 @@ Conference::Conference(const std::shared_ptr& account) if (!acc) return; ConfInfo newInfo; + { + std::lock_guard lock(shared->confInfoMutex_); + newInfo.w = shared->confInfo_.w; + newInfo.h = shared->confInfo_.h; + newInfo.layout = shared->confInfo_.layout; + } auto hostAdded = false; // Handle participants showing their video std::unique_lock lk(shared->videoToCallMtx_); @@ -116,37 +122,42 @@ Conference::Conference(const std::shared_ptr& account) if (auto* transport = call->getTransport()) deviceId = transport->deviceId(); } - if (auto videoMixer = shared->videoMixer_) - active = videoMixer->verifyActive(info.id); std::string_view peerId = string_remove_suffix(uri, '@'); auto isModerator = shared->isModerator(peerId); auto isHandRaised = shared->isHandRaised(peerId); - auto isModeratorMuted = shared->isMuted(peerId); + auto isModeratorMuted = shared->isMuted(info.id); auto sinkId = shared->getConfId() + peerId; + if (auto videoMixer = shared->videoMixer_) + active = videoMixer->verifyActive(info.id); // TODO streamId newInfo.emplace_back(ParticipantInfo {std::move(uri), - deviceId, - std::move(sinkId), - active, - info.x, - info.y, - info.w, - info.h, - !info.hasVideo, - isLocalMuted, - isModeratorMuted, - isModerator, - isHandRaised}); + deviceId, + std::move(sinkId), + active, + info.x, + info.y, + info.w, + info.h, + !info.hasVideo, + isLocalMuted, + isModeratorMuted, + isModerator, + isHandRaised}); } else { auto it = shared->videoToCall_.find(info.source); if (it == shared->videoToCall_.end()) it = shared->videoToCall_.emplace_hint(it, info.source, std::string()); // If not local + auto isModeratorMuted = false; 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 + // TODO sinkId + auto isModeratorMuted = shared->isMuted(it->second); + if (auto videoMixer = shared->videoMixer_) + active = videoMixer->verifyActive(it->second); if (auto call = std::dynamic_pointer_cast(getCall(it->second))) { uri = call->getPeerNumber(); isLocalMuted = call->isPeerMuted(); @@ -154,9 +165,8 @@ Conference::Conference(const std::shared_ptr& account) deviceId = transport->deviceId(); } } - if (auto videoMixer = shared->videoMixer_) - active = videoMixer->verifyActive(info.source); std::string_view peerId = string_remove_suffix(uri, '@'); + // TODO (another patch): use deviceId instead of peerId as specified in protocol auto isModerator = shared->isModerator(peerId); if (uri.empty() && !hostAdded) { hostAdded = true; @@ -164,22 +174,23 @@ Conference::Conference(const std::shared_ptr& account) deviceId = acc->currentDeviceId(); isLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); } - auto isHandRaised = shared->isHandRaised(peerId); - auto isModeratorMuted = shared->isMuted(peerId); + auto isHandRaised = shared->isHandRaised(deviceId); auto sinkId = shared->getConfId() + peerId; + if (auto videoMixer = shared->videoMixer_) + active |= videoMixer->verifyActive(info.source); newInfo.emplace_back(ParticipantInfo {std::move(uri), - deviceId, - std::move(sinkId), - active, - info.x, - info.y, - info.w, - info.h, - !info.hasVideo, - isLocalMuted, - isModeratorMuted, - isModerator, - isHandRaised}); + deviceId, + std::move(sinkId), + active, + info.x, + info.y, + info.w, + info.h, + !info.hasVideo, + isLocalMuted, + isModeratorMuted, + isModerator, + isHandRaised}); } } if (auto videoMixer = shared->videoMixer_) { @@ -199,6 +210,38 @@ Conference::Conference(const std::shared_ptr& account) }); }); #endif + + parser_.onVersion([&](uint32_t) {}); // TODO + parser_.onCheckAuthorization([&](std::string_view peerId) { return isModerator(peerId); }); + parser_.onHangupParticipant([&](const auto& accountUri, const auto& deviceId) { + hangupParticipant(accountUri, deviceId); + }); + parser_.onRaiseHand( + [&](const auto& deviceId, bool state) { setHandRaised(deviceId, state); }); + parser_.onSetActiveStream([&](const auto& accountUri, const auto& deviceId, bool state) { + // TODO replace per streamId + if (auto call = getCallWith(accountUri, deviceId)) + setHandRaised(call->getCallId(), state); + }); + parser_.onMuteStreamAudio + ( + [&](const auto& accountUri, const auto& deviceId, const auto& streamId, bool state) { + muteStream(accountUri, deviceId, streamId, state); + }); + parser_.onSetLayout([&](int layout) { setLayout(layout); }); + + // Version 0, deprecated + parser_.onKickParticipant( + [&](const auto& participantId) { hangupParticipant(participantId); }); + parser_.onSetActiveParticipant( + [&](const auto& participantId) { setActiveParticipant(participantId); }); + parser_.onMuteParticipant( + [&](const auto& participantId, bool state) { muteParticipant(participantId, state); }); + parser_.onRaiseHandUri([&](const auto& uri, bool state) { + if (auto call = std::dynamic_pointer_cast(getCallFromPeerID(uri))) + if (auto* transport = call->getTransport()) + setHandRaised(std::string(transport->deviceId()), state); + }); } Conference::~Conference() @@ -336,7 +379,11 @@ Conference::createConfAVStreams() // Preview if (auto& videoPreview = videoMixer_->getVideoLocal()) { auto previewSubject = std::make_shared(pluginVideoMap_); - StreamData previewStreamData {getConfId(), false, StreamType::video, getConfId(), accountId}; + StreamData previewStreamData {getConfId(), + false, + StreamType::video, + getConfId(), + accountId}; createConfAVStream(previewStreamData, *videoPreview, previewSubject); } } @@ -623,7 +670,7 @@ Conference::addParticipant(const std::string& participant_id) // Check if participant was muted before conference if (auto call = getCall(participant_id)) { if (call->isPeerMuted()) { - participantsMuted_.emplace(string_remove_suffix(call->getPeerNumber(), '@')); + participantsMuted_.emplace(call->getCallId()); } // NOTE: @@ -662,8 +709,9 @@ Conference::addParticipant(const std::string& participant_id) // call, it must be listed in the audioonlylist. auto mediaList = call->getMediaAttributeList(); if (not MediaAttribute::hasMediaType(mediaList, MediaType::MEDIA_VIDEO)) { - if (videoMixer_) + if (videoMixer_) { videoMixer_->addAudioOnlySource(call->getCallId()); + } } call->enterConference(shared_from_this()); // Continue the recording for the conference if one participant was recording @@ -691,14 +739,14 @@ Conference::setActiveParticipant(const std::string& participant_id) if (!videoMixer_) return; if (isHost(participant_id)) { - videoMixer_->setActiveHost(); + videoMixer_->addActiveHost(); return; } if (auto call = getCallFromPeerID(participant_id)) { if (auto videoRecv = call->getReceiveVideoFrameActiveWriter()) - videoMixer_->setActiveParticipant(videoRecv.get()); + videoMixer_->setActiveStream(videoRecv.get()); else - videoMixer_->setActiveParticipant(call->getCallId()); + videoMixer_->setActiveStream(call->getCallId()); return; } @@ -709,7 +757,32 @@ Conference::setActiveParticipant(const std::string& participant_id) return; } // Unset active participant by default - videoMixer_->resetActiveParticipant(); + videoMixer_->resetActiveStream(); +#endif +} + +void +Conference::setActiveStream(const std::string& streamId, bool state) +{ + // TODO BUG: for now activeStream is the callId, and should be the sink! +#ifdef ENABLE_VIDEO + if (!videoMixer_) + return; + if (state) { + // TODO remove + if (streamId.empty()) { + videoMixer_->addActiveHost(); + } else if (auto call = getCall(streamId)) { + if (auto videoRecv = call->getReceiveVideoFrameActiveWriter()) + videoMixer_->setActiveStream(videoRecv.get()); + else + videoMixer_->setActiveStream(call->getCallId()); + return; + } + // TODO videoMixer_->setActiveStream(sinkId); + } else { + videoMixer_->resetActiveStream(); + } #endif } @@ -717,21 +790,17 @@ void Conference::setLayout(int layout) { #ifdef ENABLE_VIDEO - switch (layout) { - case 0: - videoMixer_->setVideoLayout(video::Layout::GRID); - // The layout shouldn't have an active participant - videoMixer_->resetActiveParticipant(); - break; - case 1: - videoMixer_->setVideoLayout(video::Layout::ONE_BIG_WITH_SMALL); - break; - case 2: - videoMixer_->setVideoLayout(video::Layout::ONE_BIG); - break; - default: - break; + if (layout < 0 || layout > 2) { + JAMI_ERR("Unknown layout %u", layout); + return; } + if (!videoMixer_) + return; + { + std::lock_guard lk(confInfoMutex_); + confInfo_.layout = layout; + } + videoMixer_->setVideoLayout(static_cast(layout)); #endif } @@ -754,6 +823,8 @@ ConfInfo::toString() const } val["w"] = w; val["h"] = h; + val["v"] = v; + val["layout"] = layout; return Json::writeString(Json::StreamWriterBuilder {}, val); } @@ -833,11 +904,15 @@ Conference::removeParticipant(const std::string& participant_id) return; } if (auto call = getCall(participant_id)) { - if (videoMixer_->verifyActive(call->getCallId())) - videoMixer_->resetActiveParticipant(); - participantsMuted_.erase(std::string(string_remove_suffix(call->getPeerNumber(), '@'))); - handsRaised_.erase(std::string(string_remove_suffix(call->getPeerNumber(), '@'))); + auto peerId = std::string(string_remove_suffix(call->getPeerNumber(), '@')); + participantsMuted_.erase(call->getCallId()); + handsRaised_.erase(peerId); #ifdef ENABLE_VIDEO + auto sinkId = getConfId() + peerId; + // Remove if active + // TODO if (videoMixer_->verifyActive(sinkId)) + if (videoMixer_->verifyActive(participant_id)) + videoMixer_->resetActiveStream(); call->exitConference(); if (call->isPeerRecording()) call->peerRecording(false); @@ -857,7 +932,7 @@ Conference::attachLocalParticipant() auto& rbPool = Manager::instance().getRingBufferPool(); for (const auto& participant : getParticipantList()) { if (auto call = Manager::instance().getCallFromCallID(participant)) { - if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) + if (isMuted(call->getCallId())) rbPool.bindHalfDuplexOut(participant, RingBufferPool::DEFAULT_ID); else rbPool.bindCallID(participant, RingBufferPool::DEFAULT_ID); @@ -927,7 +1002,7 @@ Conference::bindParticipant(const std::string& participant_id) if (participant_id != item) { // Do not attach muted participants if (auto call = Manager::instance().getCallFromCallID(item)) { - if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) + if (isMuted(call->getCallId())) rbPool.bindHalfDuplexOut(item, participant_id); else rbPool.bindCallID(participant_id, item); @@ -963,7 +1038,7 @@ Conference::bindHost() for (const auto& item : getParticipantList()) { if (auto call = Manager::instance().getCallFromCallID(item)) { - if (isMuted(string_remove_suffix(call->getPeerNumber(), '@'))) + if (isMuted(call->getCallId())) continue; rbPool.bindCallID(item, RingBufferPool::DEFAULT_ID); rbPool.flush(RingBufferPool::DEFAULT_ID); @@ -1026,7 +1101,11 @@ Conference::switchInput(const std::string& input) // Preview if (auto& videoPreview = mixer->getVideoLocal()) { auto previewSubject = std::make_shared(pluginVideoMap_); - StreamData previewStreamData {getConfId(), false, StreamType::video, getConfId(), getAccountId()}; + StreamData previewStreamData {getConfId(), + false, + StreamType::video, + getConfId(), + getAccountId()}; createConfAVStream(previewStreamData, *videoPreview, previewSubject, true); } #endif @@ -1113,7 +1192,7 @@ 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(), '@'); + auto peerId = string_remove_suffix(call->getPeerNumber(), '@'); std::string err; Json::Value root; @@ -1121,41 +1200,13 @@ Conference::onConfOrder(const std::string& callId, const std::string& confOrder) auto reader = std::unique_ptr(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()); + (int) peerId.size(), + peerId.data()); return; } - if (root.isMember("handRaised")) { - auto state = root["handState"].asString() == "true"; - if (peerID == root["handRaised"].asString()) { - // In this case, the user want to change their state - setHandRaised(root["handRaised"].asString(), state); - } else if (!state && isModerator(peerID)) { - // In this case a moderator can lower the hand - setHandRaised(root["handRaised"].asString(), state); - } - } - - if (!isModerator(peerID)) { - JAMI_WARN("Received conference order from a non master (%.*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()); - } + parser_.initData(std::move(root), peerId); + parser_.parse(); } } @@ -1267,9 +1318,63 @@ Conference::foreachCall(const std::function& ca } bool -Conference::isMuted(std::string_view uri) const +Conference::isMuted(std::string_view callId) const { - return participantsMuted_.find(uri) != participantsMuted_.end(); + return participantsMuted_.find(callId) != participantsMuted_.end(); +} + +void +Conference::muteStream(const std::string& accountUri, + const std::string& deviceId, + const std::string&, + const bool& state) +{ + if (auto acc = std::dynamic_pointer_cast(account_.lock())) { + if (accountUri == acc->getUsername() && deviceId == acc->currentDeviceId()) { + muteHost(state); + } else if (auto call = getCallWith(accountUri, deviceId)) { + muteCall(call->getCallId(), state); + } else { + JAMI_WARN("No call with %s - %s", accountUri.c_str(), deviceId.c_str()); + } + } +} + +void +Conference::muteHost(bool state) +{ + 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(); +} + +void +Conference::muteCall(const std::string& callId, bool state) +{ + auto isPartMuted = isMuted(callId); + if (state and not isPartMuted) { + JAMI_DBG("Mute participant %.*s", (int) callId.size(), callId.data()); + participantsMuted_.emplace(callId); + unbindParticipant(callId); + updateMuted(); + } else if (not state and isPartMuted) { + JAMI_DBG("Unmute participant %.*s", (int) callId.size(), callId.data()); + participantsMuted_.erase(callId); + bindParticipant(callId); + updateMuted(); + } } void @@ -1295,42 +1400,12 @@ Conference::muteParticipant(const std::string& participant_id, const bool& state } } - // 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; - } + // NOTE: For now we only have one audio per call, and no way to only + // mute one stream + if (isHost(participant_id)) + muteHost(state); + else if (auto call = getCallFromPeerID(participant_id)) + muteCall(call->getCallId(), state); } void @@ -1338,15 +1413,13 @@ Conference::updateMuted() { std::lock_guard lk(confInfoMutex_); for (auto& info : confInfo_) { - auto peerID = string_remove_suffix(info.uri, '@'); - if (peerID.empty()) { - peerID = "host"sv; - info.audioModeratorMuted = isMuted(peerID); + if (info.uri.empty()) { + info.audioModeratorMuted = isMuted("host"sv); info.audioLocalMuted = isMediaSourceMuted(MediaType::MEDIA_AUDIO); - } else { - info.audioModeratorMuted = isMuted(peerID); - if (auto call = getCallFromPeerID(peerID)) - info.audioLocalMuted = call->isPeerMuted(); + } else if (auto call = getCallWith(std::string(string_remove_suffix(info.uri, '@')), + info.device)) { + info.audioModeratorMuted = isMuted(call->getCallId()); + info.audioLocalMuted = call->isPeerMuted(); } } sendConferenceInfos(); @@ -1412,36 +1485,34 @@ Conference::updateConferenceInfo(ConfInfo confInfo) } void -Conference::hangupParticipant(const std::string& participant_id) +Conference::hangupParticipant(const std::string& accountUri, const std::string& deviceId) { - if (isHost(participant_id)) { - Manager::instance().detachLocalParticipant(shared_from_this()); - return; - } - - if (auto call = getCallFromPeerID(participant_id)) { - if (auto account = call->getAccount().lock()) { - Manager::instance().hangupCall(account->getAccountID(), 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) + if (auto acc = std::dynamic_pointer_cast(account_.lock())) { + if (deviceId.empty()) { + // If deviceId is empty, hangup all calls with device + while (auto call = getCallFromPeerID(accountUri)) { + Manager::instance().hangupCall(acc->getAccountID(), call->getCallId()); + } return; - - Json::Value root; - root["hangupParticipant"] = participant_id; - call->sendConfOrder(root); - return; + } else { + if (accountUri == acc->getUsername() && deviceId == acc->currentDeviceId()) { + Manager::instance().detachLocalParticipant(shared_from_this()); + return; + } else if (auto call = getCallWith(accountUri, deviceId)) { + Manager::instance().hangupCall(acc->getAccountID(), call->getCallId()); + return; + } + } + // Else, it may be a remote host + auto remoteHost = findHostforRemoteParticipant(accountUri, deviceId); + if (remoteHost.empty()) { + JAMI_WARN("Can't hangup %s, peer not found", accountUri.c_str()); + return; + } + if (auto call = getCallFromPeerID(string_remove_suffix(remoteHost, '@'))) { + // Forward to the remote host. + DRing::hangupParticipant(acc->getAccountID(), call->getCallId(), accountUri, deviceId); + } } } @@ -1580,11 +1651,11 @@ Conference::mergeConfInfo(ConfInfo& newInfo, const std::string& peerURI) } std::string_view -Conference::findHostforRemoteParticipant(std::string_view uri) +Conference::findHostforRemoteParticipant(std::string_view uri, std::string_view deviceId) { for (const auto& host : remoteHosts_) { for (const auto& p : host.second) { - if (uri == string_remove_suffix(p.uri, '@')) + if (uri == string_remove_suffix(p.uri, '@') && (deviceId == "" || deviceId == p.device)) return host.first; } } @@ -1603,4 +1674,19 @@ Conference::getCallFromPeerID(std::string_view peerID) return nullptr; } +std::shared_ptr +Conference::getCallWith(const std::string& accountUri, const std::string& deviceId) +{ + for (const auto& p : getParticipantList()) { + if (auto call = std::dynamic_pointer_cast(getCall(p))) { + auto* transport = call->getTransport(); + if (accountUri == string_remove_suffix(call->getPeerNumber(), '@') && transport + && deviceId == transport->deviceId()) { + return call; + } + } + } + return {}; +} + } // namespace jami diff --git a/src/conference.h b/src/conference.h index 86d75aeed..4e28ca25d 100644 --- a/src/conference.h +++ b/src/conference.h @@ -33,6 +33,7 @@ #include #include "audio/audio_input.h" +#include "conference_protocol.h" #include "media_attribute.h" #include @@ -147,6 +148,8 @@ struct ConfInfo : public std::vector { int h {0}; int w {0}; + int v {1}; // Supported conference protocol version + int layout {0}; friend bool operator==(const ConfInfo& c1, const ConfInfo& c2) { @@ -323,6 +326,7 @@ public: void switchSecondaryInput(const std::string& input); void setActiveParticipant(const std::string& participant_id); + void setActiveStream(const std::string& streamId, bool state); void setLayout(int layout); void onConfOrder(const std::string& callId, const std::string& order); @@ -345,14 +349,31 @@ public: void updateConferenceInfo(ConfInfo confInfo); void setModerator(const std::string& uri, const bool& state); + void hangupParticipant(const std::string& accountUri, const std::string& deviceId = ""); 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(); void muteLocalHost(bool is_muted, const std::string& mediaType); bool isRemoteParticipant(const std::string& uri); void mergeConfInfo(ConfInfo& newInfo, const std::string& peerURI); + /** + * The client shows one tile per stream (video/audio related to a media) + * @note for now, in conferences we can only mute the audio of a call + * @todo add a track (audio OR video) parameter to know what we want to mute + * @param accountUri Account of the stream + * @param deviceId Device of the stream + * @param streamId Stream to mute + * @param state True to mute, false to unmute + */ + void muteStream(const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); + void updateMuted(); + + std::shared_ptr getCallFromPeerID(std::string_view peerID); + private: std::weak_ptr weak() { @@ -364,6 +385,8 @@ private: bool isHandRaised(std::string_view uri) const; void updateModerators(); void updateHandsRaised(); + void muteHost(bool state); + void muteCall(const std::string& callId, bool state); void foreachCall(const std::function& call)>& cb); @@ -424,8 +447,10 @@ private: #ifdef ENABLE_VIDEO void resizeRemoteParticipants(ConfInfo& confInfo, std::string_view peerURI); #endif - std::string_view findHostforRemoteParticipant(std::string_view uri); - std::shared_ptr getCallFromPeerID(std::string_view peerID); + std::string_view findHostforRemoteParticipant(std::string_view uri, + std::string_view deviceId = ""); + + std::shared_ptr getCallWith(const std::string& accountUri, const std::string& deviceId); std::mutex sinksMtx_ {}; @@ -466,6 +491,8 @@ private: std::mutex avStreamsMtx_ {}; std::map> confAVStreams; #endif // ENABLE_PLUGIN + + ConfProtocolParser parser_; }; } // namespace jami diff --git a/src/conference_protocol.cpp b/src/conference_protocol.cpp new file mode 100644 index 000000000..e7652487e --- /dev/null +++ b/src/conference_protocol.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin + * + * 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 "conference_protocol.h" + +#include "string_utils.h" + +namespace jami { + +namespace ProtocolKeys { + +constexpr static const char* PROTOVERSION = "version"; +constexpr static const char* LAYOUT = "layout"; +// V0 +constexpr static const char* HANDRAISED = "handRaised"; +constexpr static const char* HANDSTATE = "handState"; +constexpr static const char* ACTIVEPART = "activeParticipant"; +constexpr static const char* MUTEPART = "muteParticipant"; +constexpr static const char* MUTESTATE = "muteState"; +constexpr static const char* HANGUPPART = "hangupParticipant"; +// V1 +constexpr static const char* DEVICES = "devices"; +constexpr static const char* MEDIAS = "medias"; +constexpr static const char* RAISEHAND = "raiseHand"; +constexpr static const char* HANGUP = "hangup"; +constexpr static const char* ACTIVE = "active"; +constexpr static const char* MUTEAUDIO = "muteAudio"; +// Future +constexpr static const char* MUTEVIDEO = "muteVideo"; + +} // namespace ProtocolKeys + +void +ConfProtocolParser::parse() +{ + if (data_.isMember(ProtocolKeys::PROTOVERSION)) { + uint32_t version = data_[ProtocolKeys::PROTOVERSION].asUInt(); + if (version_) version_(version); + if (version == 1) { + parseV1(); + } else { + JAMI_WARN() << "Unsupported protocol version " << version; + } + } else { + parseV0(); + } +} + +void +ConfProtocolParser::parseV0() +{ + if (!checkAuthorization_ || !raiseHandUri_ || !setLayout_ || !setActiveParticipant_ + || !muteParticipant_ || !kickParticipant_) { + JAMI_ERR() << "Missing methods for ConfProtocolParser"; + return; + } + // Check if all lambdas set + auto isPeerModerator = checkAuthorization_(peerId_); + if (data_.isMember(ProtocolKeys::HANDRAISED)) { + auto state = data_[ProtocolKeys::HANDSTATE].asString() == TRUE_STR; + std::string deviceId; + auto uri = data_[ProtocolKeys::HANDRAISED].asString(); + if (peerId_ == uri) { + // In this case, the user want to change their state + raiseHandUri_(deviceId, state); + } else if (!state && isPeerModerator) { + // In this case a moderator can lower the hand + raiseHandUri_(deviceId, state); + } + } + if (!isPeerModerator) { + JAMI_WARN("Received conference order from a non master (%.*s)", + (int) peerId_.size(), + peerId_.data()); + return; + } + if (data_.isMember(ProtocolKeys::LAYOUT)) { + setLayout_(data_[ProtocolKeys::LAYOUT].asInt()); + } + if (data_.isMember(ProtocolKeys::ACTIVEPART)) { + setActiveParticipant_(data_[ProtocolKeys::ACTIVEPART].asString()); + } + if (data_.isMember(ProtocolKeys::MUTEPART) && data_.isMember(ProtocolKeys::MUTESTATE)) { + muteParticipant_(data_[ProtocolKeys::MUTEPART].asString(), + data_[ProtocolKeys::MUTESTATE].asString() == TRUE_STR); + } + if (data_.isMember(ProtocolKeys::HANGUPPART)) { + kickParticipant_(data_[ProtocolKeys::HANGUPPART].asString()); + } +} + +void +ConfProtocolParser::parseV1() +{ + if (!checkAuthorization_ || !setLayout_ || !raiseHand_ || !hangupParticipant_ || !muteStreamAudio_ + || !setActiveStream_) { + JAMI_ERR() << "Missing methods for ConfProtocolParser"; + return; + } + + auto isPeerModerator = checkAuthorization_(peerId_); + for (Json::Value::const_iterator itr = data_.begin(); itr != data_.end(); itr++) { + auto key = itr.key(); + if (isPeerModerator && key == ProtocolKeys::LAYOUT) { + // Note: can be removed soon + setLayout_(itr->asInt()); + } else { + auto accValue = *itr; + if (accValue.isMember(ProtocolKeys::DEVICES)) { + auto accountUri = key.asString(); + for (Json::Value::const_iterator itrd = accValue[ProtocolKeys::DEVICES].begin(); + itrd != accValue[ProtocolKeys::DEVICES].end(); + itrd++) { + auto deviceId = itrd.key().asString(); + auto deviceValue = *itrd; + if (deviceValue.isMember(ProtocolKeys::RAISEHAND)) { + auto newState = deviceValue[ProtocolKeys::RAISEHAND].asBool(); + if (peerId_ == accountUri || (!newState && isPeerModerator)) + raiseHand_(deviceId, newState); + } + if (isPeerModerator && deviceValue.isMember(ProtocolKeys::HANGUP)) { + hangupParticipant_(accountUri, deviceId); + } + if (deviceValue.isMember(ProtocolKeys::MEDIAS)) { + for (Json::Value::const_iterator itrm = accValue[ProtocolKeys::MEDIAS].begin(); + itrm != accValue[ProtocolKeys::MEDIAS].end(); + itrm++) { + auto streamId = itrm.key().asString(); + auto mediaVal = *itrm; + if (isPeerModerator) { + if (mediaVal.isMember(ProtocolKeys::MUTEVIDEO) && !muteStreamVideo_) { + // Note: For now, it's not implemented so not set + muteStreamVideo_(accountUri, + deviceId, + streamId, + mediaVal[ProtocolKeys::MUTEVIDEO].asBool()); + } + if (mediaVal.isMember(ProtocolKeys::MUTEAUDIO)) { + muteStreamAudio_(accountUri, + deviceId, + streamId, + mediaVal[ProtocolKeys::MUTEAUDIO].asBool()); + } + if (mediaVal.isMember(ProtocolKeys::ACTIVE)) { + // TODO streamId + setActiveStream_(accountUri, + deviceId, + mediaVal[ProtocolKeys::ACTIVE].asBool()); + } + } + } + } + } + } + } + } +} + +} // namespace jami diff --git a/src/conference_protocol.h b/src/conference_protocol.h new file mode 100644 index 000000000..cee480826 --- /dev/null +++ b/src/conference_protocol.h @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin + * + * 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. + */ +#pragma once + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +#include "logger.h" + +namespace jami { + +/** + * Used to parse confOrder objects + * @note the user of this class must initialize the different lambdas. + */ +class ConfProtocolParser { + +public: + ConfProtocolParser() {}; + + void onVersion(std::function&& cb) + { + version_ = std::move(cb); + } + /** + * Ask the caller to check if a peer is authorized (moderator of the conference) + */ + void onCheckAuthorization(std::function&& cb) { + checkAuthorization_ = std::move(cb); + } + + void onHangupParticipant(std::function&& cb) + { + hangupParticipant_ = std::move(cb); + } + void onRaiseHand(std::function&& cb) + { + raiseHand_ = std::move(cb); + } + /** + * @todo, replace the 2 strings per streamId + */ + void onSetActiveStream(std::function&& cb) + { + setActiveStream_ = std::move(cb); + } + void onMuteStreamAudio( + std::function&& cb) + { + muteStreamAudio_ = std::move(cb); + } + void onMuteStreamVideo( + std::function&& cb) + { + muteStreamVideo_ = std::move(cb); + } + void onSetLayout(std::function&& cb) + { + setLayout_ = std::move(cb); + } + + // Version 0, deprecated + void onKickParticipant(std::function&& cb) + { + kickParticipant_ = std::move(cb); + } + void onSetActiveParticipant(std::function&& cb) + { + setActiveParticipant_ = std::move(cb); + } + void onMuteParticipant(std::function&& cb) + { + muteParticipant_ = std::move(cb); + } + void onRaiseHandUri(std::function&& cb) + { + raiseHandUri_ = std::move(cb); + } + + /** + * Inject in the parser the data to parse + */ + void initData(Json::Value&& d, std::string_view peerId) + { + data_ = std::move(d); + peerId_ = peerId; + } + + /** + * Parse the datas, this will call the methods injected if necessary + */ + void parse(); + +private: + void parseV0(); + void parseV1(); + + std::string_view peerId_; + Json::Value data_; + + std::function version_; + + std::function checkAuthorization_; + std::function hangupParticipant_; + std::function raiseHand_; + std::function setActiveStream_; + std::function + muteStreamAudio_; + std::function + muteStreamVideo_; + std::function setLayout_; + + std::function raiseHandUri_; + std::function kickParticipant_; + std::function setActiveParticipant_; + std::function muteParticipant_; +}; + +} // namespace jami diff --git a/src/jami/callmanager_interface.h b/src/jami/callmanager_interface.h index 222c4ce39..053e44161 100644 --- a/src/jami/callmanager_interface.h +++ b/src/jami/callmanager_interface.h @@ -96,9 +96,6 @@ DRING_PUBLIC void createConfFromParticipantList(const std::string& accountId, DRING_PUBLIC void setConferenceLayout(const std::string& accountId, const std::string& confId, uint32_t layout); -DRING_PUBLIC void setActiveParticipant(const std::string& accountId, - const std::string& confId, - const std::string& callId); DRING_PUBLIC bool isConferenceParticipant(const std::string& accountId, const std::string& callId); DRING_PUBLIC bool addParticipant(const std::string& accountId, const std::string& callId, @@ -124,19 +121,44 @@ DRING_PUBLIC std::vector> getConferenceInfos( const std::string& accountId, const std::string& confId); DRING_PUBLIC void setModerator(const std::string& accountId, const std::string& confId, - const std::string& peerId, + const std::string& accountUri, const bool& state); +/// DEPRECATED USE muteStream DRING_PUBLIC void muteParticipant(const std::string& accountId, const std::string& confId, - const std::string& peerId, + const std::string& accountUri, + const bool& state); +// Note: muting Audio not supported yet +DRING_PUBLIC void muteStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, + const bool& state); +/// DEPRECATED, USE setActiveStream +DRING_PUBLIC void setActiveParticipant(const std::string& accountId, + const std::string& confId, + const std::string& callId); +DRING_PUBLIC void setActiveStream(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const std::string& streamId, const bool& state); DRING_PUBLIC void hangupParticipant(const std::string& accountId, const std::string& confId, - const std::string& participant); + const std::string& accountUri, + const std::string& deviceId); +/// DEPRECATED, use raiseHand DRING_PUBLIC void raiseParticipantHand(const std::string& accountId, const std::string& confId, const std::string& peerId, const bool& state); +DRING_PUBLIC void raiseHand(const std::string& accountId, + const std::string& confId, + const std::string& accountUri, + const std::string& deviceId, + const bool& state); /* Statistic related methods */ DRING_PUBLIC void startSmartInfo(uint32_t refreshTimeMs); diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp index f45971c89..d0f2515d9 100644 --- a/src/media/video/video_mixer.cpp +++ b/src/media/video/video_mixer.cpp @@ -178,32 +178,34 @@ VideoMixer::stopInput() } void -VideoMixer::setActiveHost() +VideoMixer::addActiveHost() { - activeAudioOnly_ = ""; + activeStream_ = ""; activeSource_ = videoLocalSecondary_ ? videoLocalSecondary_.get() : videoLocal_.get(); updateLayout(); } void -VideoMixer::setActiveParticipant(Observable>* ob) +VideoMixer::setActiveStream(Observable>* ob) { - activeAudioOnly_ = ""; + activeStream_ = ""; activeSource_ = ob; updateLayout(); } void -VideoMixer::setActiveParticipant(const std::string& id) +VideoMixer::setActiveStream(const std::string& id) { - activeAudioOnly_ = id; activeSource_ = nullptr; + activeStream_ = id; updateLayout(); } void VideoMixer::updateLayout() { + if (activeStream_ == "" && activeSource_ == nullptr) + currentLayout_ = Layout::GRID; layoutUpdated_ += 1; } @@ -229,9 +231,8 @@ VideoMixer::detached(Observable>* ob) for (const auto& x : sources_) { if (x->source == ob) { // Handle the case where the current shown source leave the conference - if (activeSource_ == ob) { - resetActiveParticipant(); - } + if (verifyActive(ob)) + resetActiveStream(); JAMI_DBG("Remove source [%p]", x.get()); sources_.remove(x); JAMI_DBG("Total sources: %lu", sources_.size()); @@ -303,10 +304,12 @@ VideoMixer::process() sourcesInfo.reserve(sources_.size() + audioOnlySources_.size()); // add all audioonlysources for (auto& id : audioOnlySources_) { - if (currentLayout_ != Layout::ONE_BIG or activeAudioOnly_ == id) { + JAMI_ERR() << "@@@ " << id; + auto active = verifyActive(id); + if (currentLayout_ != Layout::ONE_BIG or active) { sourcesInfo.emplace_back(SourceInfo {{}, 0, 0, 10, 10, false, id}); } - if (currentLayout_ == Layout::ONE_BIG and activeAudioOnly_ == id) + if (currentLayout_ == Layout::ONE_BIG and active) successfullyRendered = true; } // add video sources @@ -315,7 +318,8 @@ VideoMixer::process() if (!loop_.isRunning()) return; - if (currentLayout_ != Layout::ONE_BIG or activeSource_ == x->source) { + auto activeSource = verifyActive(x->source); + if (currentLayout_ != Layout::ONE_BIG or activeSource) { // make rendered frame temporarily unavailable for update() // to avoid concurrent access. std::shared_ptr input = x->getRenderFrame(); @@ -326,7 +330,7 @@ VideoMixer::process() wantedIndex = 0; activeFound = true; } else if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) { - if (activeSource_ == x->source) { + if (activeSource) { wantedIndex = 0; activeFound = true; } else if (not activeFound) { diff --git a/src/media/video/video_mixer.h b/src/media/video/video_mixer.h index d271c9fd4..bbd982ddd 100644 --- a/src/media/video/video_mixer.h +++ b/src/media/video/video_mixer.h @@ -73,31 +73,37 @@ public: void switchSecondaryInput(const std::string& input); void stopInput(); - void setActiveParticipant(Observable>* ob); - void setActiveParticipant(const std::string& id); - void resetActiveParticipant() { - activeAudioOnly_ = ""; - activeSource_ = nullptr; + void setActiveStream(Observable>* ob); + void setActiveStream(const std::string& id); + void resetActiveStream() + { + activeStream_ = {}; + activeSource_ = {}; updateLayout(); } - void setActiveHost(); + void addActiveHost(); + // TODO group, we can only use a set of string to identify actives bool verifyActive(const std::string& id) { - return id == activeAudioOnly_; + return activeStream_ == id; } bool verifyActive(Observable>* ob) { - return ob == activeSource_; + return activeSource_ == ob; } void setVideoLayout(Layout newLayout) { currentLayout_ = newLayout; + if (currentLayout_ == Layout::GRID) + resetActiveStream(); layoutUpdated_ += 1; } + Layout getVideoLayout() const { return currentLayout_; } + void setOnSourcesUpdated(OnSourcesUpdatedCb&& cb) { onSourcesUpdated_ = std::move(cb); } MediaStream getStream(const std::string& name) const; @@ -118,8 +124,8 @@ public: void removeAudioOnlySource(const std::string& id) { std::lock_guard lk(audioOnlySourcesMtx_); - audioOnlySources_.erase(id); - updateLayout(); + if (audioOnlySources_.erase(id)) + updateLayout(); } private: @@ -161,7 +167,7 @@ private: std::mutex audioOnlySourcesMtx_; std::set audioOnlySources_; - std::string activeAudioOnly_{}; + std::string activeStream_ {}; std::atomic_int layoutUpdated_ {0}; OnSourcesUpdatedCb onSourcesUpdated_ {}; diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp index ffb719427..df09ddbec 100644 --- a/src/media/video/video_rtp_session.cpp +++ b/src/media/video/video_rtp_session.cpp @@ -260,7 +260,7 @@ VideoRtpSession::startReceiver() || videoMixer_->verifyActive(callID_); videoMixer_->removeAudioOnlySource(callID_); if (activeParticipant) - videoMixer_->setActiveParticipant(receiveThread_.get()); + videoMixer_->setActiveStream(receiveThread_.get()); } } else { @@ -271,7 +271,7 @@ VideoRtpSession::startReceiver() videoMixer_->addAudioOnlySource(callID_); receiveThread_->detach(videoMixer_.get()); if (activeParticipant) - videoMixer_->setActiveParticipant(callID_); + videoMixer_->setActiveStream(callID_); } } if (socketPair_) @@ -293,7 +293,7 @@ VideoRtpSession::stopReceiver() videoMixer_->addAudioOnlySource(callID_); receiveThread_->detach(videoMixer_.get()); if (activeParticipant) - videoMixer_->setActiveParticipant(callID_); + videoMixer_->setActiveStream(callID_); } // We need to disable the read operation, otherwise the @@ -549,7 +549,7 @@ VideoRtpSession::exitConference() conference_->detachVideo(receiveThread_.get()); receiveThread_->startSink(); if (activetParticipant) - videoMixer_->setActiveParticipant(callID_); + videoMixer_->setActiveStream(callID_); } videoMixer_.reset(); diff --git a/src/meson.build b/src/meson.build index 7a53e38dd..9abdb1906 100644 --- a/src/meson.build +++ b/src/meson.build @@ -106,6 +106,7 @@ libjami_sources = files( 'call.cpp', 'call_factory.cpp', 'conference.cpp', + 'conference_protocol.cpp', 'data_transfer.cpp', 'fileutils.cpp', 'ftp_server.cpp', diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp index ba52bd4bf..aeb7ded93 100644 --- a/src/sip/sipcall.cpp +++ b/src/sip/sipcall.cpp @@ -88,6 +88,9 @@ static constexpr int ICE_COMP_COUNT_PER_STREAM {2}; static constexpr auto MULTISTREAM_REQUIRED_VERSION_STR = "10.0.2"sv; static const std::vector MULTISTREAM_REQUIRED_VERSION = split_string_to_unsigned(MULTISTREAM_REQUIRED_VERSION_STR, '.'); +static constexpr auto NEW_CONFPROTOCOL_VERSION_STR = "13.1.0"sv; +static const std::vector NEW_CONFPROTOCOL_VERSION + = split_string_to_unsigned(NEW_CONFPROTOCOL_VERSION_STR, '.'); static constexpr auto REUSE_ICE_IN_REINVITE_REQUIRED_VERSION_STR = "11.0.2"sv; static const std::vector REUSE_ICE_IN_REINVITE_REQUIRED_VERSION = split_string_to_unsigned(REUSE_ICE_IN_REINVITE_REQUIRED_VERSION_STR, '.'); diff --git a/test/unitTest/call/conference.cpp b/test/unitTest/call/conference.cpp index 447a90b27..196370f83 100644 --- a/test/unitTest/call/conference.cpp +++ b/test/unitTest/call/conference.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C)2021 Savoir-faire Linux Inc. + * Copyright (C) 2021-2022 Savoir-faire Linux Inc. * Author: Sébastien Blin * * This program is free software; you can redistribute it and/or modify @@ -34,6 +34,7 @@ #include "video/sinkclient.h" using namespace DRing::Account; +using namespace std::literals::chrono_literals; namespace jami { namespace test { @@ -43,6 +44,7 @@ struct CallData std::string callId {}; std::string state {}; std::string device {}; + std::string streamId {}; std::string hostState {}; std::atomic_bool moderatorMuted {false}; std::atomic_bool raisedHand {false}; @@ -53,6 +55,7 @@ struct CallData callId = ""; state = ""; device = ""; + streamId = ""; hostState = ""; moderatorMuted = false; active = false; @@ -78,6 +81,7 @@ public: private: void testGetConference(); void testModeratorMuteUpdateParticipantsInfos(); + void testUnauthorizedMute(); void testAudioVideoMutedStates(); void testCreateParticipantsSinks(); void testMuteStatusAfterRemove(); @@ -86,10 +90,13 @@ private: void testPeerLeaveConference(); void testJoinCallFromOtherAccount(); void testDevices(); + void testUnauthorizedSetActive(); + void testHangup(); CPPUNIT_TEST_SUITE(ConferenceTest); CPPUNIT_TEST(testGetConference); CPPUNIT_TEST(testModeratorMuteUpdateParticipantsInfos); + CPPUNIT_TEST(testUnauthorizedMute); CPPUNIT_TEST(testAudioVideoMutedStates); CPPUNIT_TEST(testCreateParticipantsSinks); CPPUNIT_TEST(testMuteStatusAfterRemove); @@ -98,6 +105,8 @@ private: CPPUNIT_TEST(testPeerLeaveConference); CPPUNIT_TEST(testJoinCallFromOtherAccount); CPPUNIT_TEST(testDevices); + CPPUNIT_TEST(testUnauthorizedSetActive); + CPPUNIT_TEST(testHangup); CPPUNIT_TEST_SUITE_END(); // Common parts @@ -220,16 +229,19 @@ ConferenceTest::registerSignalHandlers() bobCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; bobCall.raisedHand = infos.at("handRaised") == "true"; bobCall.device = infos.at("device"); + bobCall.streamId = infos.at("sinkId"); } else if (infos.at("uri").find(carlaUri) != std::string::npos) { carlaCall.active = infos.at("active") == "true"; carlaCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; carlaCall.raisedHand = infos.at("handRaised") == "true"; carlaCall.device = infos.at("device"); + carlaCall.streamId = infos.at("sinkId"); } else if (infos.at("uri").find(daviUri) != std::string::npos) { daviCall.active = infos.at("active") == "true"; daviCall.moderatorMuted = infos.at("audioModeratorMuted") == "true"; daviCall.raisedHand = infos.at("handRaised") == "true"; daviCall.device = infos.at("device"); + daviCall.streamId = infos.at("sinkId"); } } cv.notify_one(); @@ -249,27 +261,25 @@ ConferenceTest::startConference() JAMI_INFO("Start call between Alice and Bob"); auto call1 = DRing::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !bobCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !bobCall.callId.empty(); })); Manager::instance().answerCall(bobId, bobCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return bobCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return bobCall.hostState == "CURRENT"; })); JAMI_INFO("Start call between Alice and Carla"); auto call2 = DRing::placeCallWithMedia(aliceId, carlaUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !carlaCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !carlaCall.callId.empty(); })); Manager::instance().answerCall(carlaId, carlaCall.callId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(20), [&] { - return carlaCall.hostState == "CURRENT"; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return carlaCall.hostState == "CURRENT"; })); JAMI_INFO("Start conference"); confChanged = false; Manager::instance().joinParticipant(aliceId, call1, aliceId, call2); // ConfChanged is the signal emitted when the 2 calls will be added to the conference - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !confId.empty() && confChanged; })); + // Also, wait that participants appears in conf info to get all good informations + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { + return !confId.empty() && confChanged && !carlaCall.device.empty() + && !bobCall.device.empty(); + })); } void @@ -277,7 +287,7 @@ ConferenceTest::hangupConference() { JAMI_INFO("Stop conference"); Manager::instance().hangupConference(aliceId, confId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return bobCall.state == "OVER" && carlaCall.state == "OVER" && confId.empty(); })); } @@ -312,13 +322,30 @@ ConferenceTest::testModeratorMuteUpdateParticipantsInfos() startConference(); JAMI_INFO("Play with mute from the moderator"); - DRing::muteParticipant(aliceId, confId, bobUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return bobCall.moderatorMuted.load(); })); + DRing::muteStream(aliceId, confId, bobUri, bobCall.device, bobCall.streamId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return bobCall.moderatorMuted.load(); })); - DRing::muteParticipant(aliceId, confId, bobUri, false); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !bobCall.moderatorMuted.load(); })); + DRing::muteStream(aliceId, confId, bobUri, bobCall.device, bobCall.streamId, false); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !bobCall.moderatorMuted.load(); })); + + hangupConference(); + + DRing::unregisterSignalHandlers(); +} + +void +ConferenceTest::testUnauthorizedMute() +{ + registerSignalHandlers(); + + auto bobAccount = Manager::instance().getAccount(bobId); + auto bobUri = bobAccount->getUsername(); + + startConference(); + + JAMI_INFO("Play with mute from unauthorized"); + DRing::muteStream(carlaId, confId, bobUri, bobCall.device, bobCall.streamId, true); + CPPUNIT_ASSERT(!cv.wait_for(lk, 15s, [&] { return bobCall.moderatorMuted.load(); })); hangupConference(); @@ -338,23 +365,18 @@ ConferenceTest::testAudioVideoMutedStates() JAMI_INFO("Start call between Alice and Bob"); auto call1Id = DRing::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !bobCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !bobCall.callId.empty(); })); Manager::instance().answerCall(bobId, bobCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return bobCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return bobCall.hostState == "CURRENT"; })); auto call1 = aliceAccount->getCall(call1Id); call1->muteMedia(DRing::Media::MediaAttributeValue::AUDIO, true); call1->muteMedia(DRing::Media::MediaAttributeValue::VIDEO, true); JAMI_INFO("Start call between Alice and Carla"); auto call2Id = DRing::placeCallWithMedia(aliceId, carlaUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !carlaCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !carlaCall.callId.empty(); })); Manager::instance().answerCall(carlaId, carlaCall.callId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(20), [&] { - return carlaCall.hostState == "CURRENT"; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return carlaCall.hostState == "CURRENT"; })); auto call2 = aliceAccount->getCall(call2Id); call2->muteMedia(DRing::Media::MediaAttributeValue::AUDIO, true); @@ -362,13 +384,13 @@ ConferenceTest::testAudioVideoMutedStates() JAMI_INFO("Start conference"); Manager::instance().joinParticipant(aliceId, call1Id, aliceId, call2Id); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(20), [&] { return !confId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !confId.empty(); })); auto conf = aliceAccount->getConference(confId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return conf->isMediaSourceMuted(jami::MediaType::MEDIA_AUDIO); })); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return conf->isMediaSourceMuted(jami::MediaType::MEDIA_VIDEO); })); @@ -391,7 +413,7 @@ ConferenceTest::testCreateParticipantsSinks() auto infos = DRing::getConferenceInfos(aliceId, confId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { bool sinksStatus = true; for (auto& info : infos) { if (info["uri"] == bobUri) { @@ -423,42 +445,35 @@ ConferenceTest::testMuteStatusAfterRemove() JAMI_INFO("Start call between Alice and Davi"); auto call1 = DRing::placeCallWithMedia(aliceId, daviUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call1, aliceId, confId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); - DRing::muteParticipant(aliceId, confId, daviUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return daviCall.moderatorMuted.load(); })); + DRing::muteStream(aliceId, confId, daviUri, daviCall.device, daviCall.streamId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return daviCall.moderatorMuted.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); daviCall.reset(); auto call2 = DRing::placeCallWithMedia(aliceId, daviUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call2, aliceId, confId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !daviCall.moderatorMuted.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !daviCall.moderatorMuted.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); hangupConference(); DRing::unregisterSignalHandlers(); } - void ConferenceTest::testActiveStatusAfterRemove() { @@ -480,37 +495,35 @@ ConferenceTest::testActiveStatusAfterRemove() auto call1 = DRing::placeCallWithMedia(aliceId, daviUri, MediaAttribute::mediaAttributesToMediaMaps( - {defaultAudio})); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + {defaultAudio})); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call1, aliceId, confId); - DRing::setActiveParticipant(aliceId, confId, daviUri); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return daviCall.active.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); + + DRing::setActiveStream(aliceId, confId, daviUri, daviCall.device, daviCall.streamId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return daviCall.active.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); daviCall.reset(); - auto call2 = DRing::placeCallWithMedia(aliceId, daviUri, MediaAttribute::mediaAttributesToMediaMaps({defaultAudio})); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + auto call2 = DRing::placeCallWithMedia(aliceId, + daviUri, + MediaAttribute::mediaAttributesToMediaMaps( + {defaultAudio})); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call2, aliceId, confId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !daviCall.active.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !daviCall.active.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); hangupConference(); DRing::unregisterSignalHandlers(); @@ -532,68 +545,56 @@ ConferenceTest::testHandsUp() startConference(); JAMI_INFO("Play with raise hand"); - DRing::raiseParticipantHand(bobId, bobCall.callId, bobUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return bobCall.raisedHand.load(); })); + DRing::raiseHand(bobId, bobCall.callId, bobUri, bobCall.device, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return bobCall.raisedHand.load(); })); - DRing::raiseParticipantHand(bobId, bobCall.callId, bobUri, false); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !bobCall.raisedHand.load(); })); + DRing::raiseHand(bobId, bobCall.callId, bobUri, bobCall.device, false); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !bobCall.raisedHand.load(); })); JAMI_INFO("Start call between Alice and Davi"); auto call1 = DRing::placeCallWithMedia(aliceId, daviUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call1, aliceId, confId); // Remove davi from moderators DRing::setModerator(aliceId, confId, daviUri, false); // Test to raise hand - DRing::raiseParticipantHand(daviId, daviCall.callId, daviUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return daviCall.raisedHand.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); + DRing::raiseHand(daviId, daviCall.callId, daviUri, daviCall.device, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return daviCall.raisedHand.load(); })); // Test to raise hand for another one (should fail) - DRing::raiseParticipantHand(bobId, bobCall.callId, carlaUri, true); - CPPUNIT_ASSERT( - !cv.wait_for(lk, std::chrono::seconds(5), [&] { return carlaCall.raisedHand.load(); })); + DRing::raiseHand(bobId, bobCall.callId, carlaUri, carlaCall.device, true); + CPPUNIT_ASSERT(!cv.wait_for(lk, 5s, [&] { return carlaCall.raisedHand.load(); })); // However, a moderator should be able to lower the hand (but not a non moderator) - DRing::raiseParticipantHand(carlaId, carlaCall.callId, carlaUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return carlaCall.raisedHand.load(); })); + DRing::raiseHand(carlaId, carlaCall.callId, carlaUri, carlaCall.device, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return carlaCall.raisedHand.load(); })); - DRing::raiseParticipantHand(daviId, carlaCall.callId, carlaUri, false); - CPPUNIT_ASSERT( - !cv.wait_for(lk, std::chrono::seconds(5), [&] { return !carlaCall.raisedHand.load(); })); + DRing::raiseHand(daviId, carlaCall.callId, carlaUri, carlaCall.device, false); + CPPUNIT_ASSERT(!cv.wait_for(lk, 5s, [&] { return !carlaCall.raisedHand.load(); })); - DRing::raiseParticipantHand(bobId, bobCall.callId, carlaUri, false); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !carlaCall.raisedHand.load(); })); + DRing::raiseHand(bobId, bobCall.callId, carlaUri, carlaCall.device, false); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !carlaCall.raisedHand.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); daviCall.reset(); auto call2 = DRing::placeCallWithMedia(aliceId, daviUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); Manager::instance().addParticipant(aliceId, call2, aliceId, confId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !daviCall.raisedHand.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !daviCall.raisedHand.load(); })); Manager::instance().hangupCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.state == "OVER"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.state == "OVER"; })); hangupConference(); DRing::unregisterSignalHandlers(); @@ -610,9 +611,7 @@ ConferenceTest::testPeerLeaveConference() startConference(); Manager::instance().hangupCall(bobId, bobCall.callId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(20), [&] { - return bobCall.state == "OVER" && confId.empty(); - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return bobCall.state == "OVER" && confId.empty(); })); DRing::unregisterSignalHandlers(); } @@ -631,21 +630,17 @@ ConferenceTest::testJoinCallFromOtherAccount() startConference(); JAMI_INFO("Play with raise hand"); - DRing::raiseParticipantHand(aliceId, confId, bobUri, true); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return bobCall.raisedHand.load(); })); + DRing::raiseHand(aliceId, confId, bobUri, bobCall.device, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return bobCall.raisedHand.load(); })); - DRing::raiseParticipantHand(aliceId, confId, bobUri, false); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(5), [&] { return !bobCall.raisedHand.load(); })); + DRing::raiseHand(aliceId, confId, bobUri, bobCall.device, false); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return !bobCall.raisedHand.load(); })); JAMI_INFO("Start call between Alice and Davi"); auto call1 = DRing::placeCallWithMedia(aliceId, daviUri, {}); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return !daviCall.callId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(20), [&] { return daviCall.hostState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); CPPUNIT_ASSERT(Manager::instance().addParticipant(daviId, daviCall.callId, aliceId, confId)); hangupConference(); @@ -664,7 +659,7 @@ ConferenceTest::testDevices() startConference(); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { return bobCall.device == bobDevice && carlaDevice == carlaCall.device; })); @@ -673,6 +668,57 @@ ConferenceTest::testDevices() DRing::unregisterSignalHandlers(); } +void +ConferenceTest::testUnauthorizedSetActive() +{ + registerSignalHandlers(); + + auto bobAccount = Manager::instance().getAccount(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount(carlaId); + auto carlaUri = carlaAccount->getUsername(); + + startConference(); + + DRing::setActiveStream(carlaId, confId, bobUri, bobCall.device, bobCall.streamId, true); + CPPUNIT_ASSERT(!cv.wait_for(lk, 15s, [&] { return bobCall.active.load(); })); + + hangupConference(); + + DRing::unregisterSignalHandlers(); +} + +void +ConferenceTest::testHangup() +{ + registerSignalHandlers(); + + auto bobAccount = Manager::instance().getAccount(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount(carlaId); + auto daviAccount = Manager::instance().getAccount(daviId); + auto daviUri = daviAccount->getUsername(); + + startConference(); + + JAMI_INFO("Start call between Alice and Davi"); + auto call1 = DRing::placeCallWithMedia(aliceId, daviUri, {}); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); + Manager::instance().answerCall(daviId, daviCall.callId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); + Manager::instance().addParticipant(aliceId, call1, aliceId, confId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.device.empty(); })); + + DRing::hangupParticipant(carlaId, confId, daviUri, daviCall.device); // Unauthorized + CPPUNIT_ASSERT(!cv.wait_for(lk, 10s, [&] { return daviCall.state == "OVER"; })); + DRing::hangupParticipant(aliceId, confId, daviUri, daviCall.device); + CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return daviCall.state == "OVER"; })); + + hangupConference(); + + DRing::unregisterSignalHandlers(); +} + } // namespace test } // namespace jami