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