diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d22759811fb7a74ed2c2a652fc9bd4a78aa2c39..c25be7896468b9ace027b97e54319da2200baa8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,6 +277,7 @@ SET( libringclient_LIB_SRCS #Models src/contactmodel.cpp + src/callparticipantsmodel.cpp src/newcallmodel.cpp src/newdevicemodel.cpp src/newcodecmodel.cpp @@ -342,6 +343,7 @@ SET(libringclient_api_LIB_HDRS src/api/member.h src/api/newaccountmodel.h src/api/newcallmodel.h + src/api/callparticipantsmodel.h src/api/newcodecmodel.h src/api/newdevicemodel.h src/api/pluginmodel.h diff --git a/src/api/call.h b/src/api/call.h index 993bf4a51751f0e6ca58592e05c62b059e47e2f4..54741a966dab3bca1891bb42007561bfd7b6aba9 100644 --- a/src/api/call.h +++ b/src/api/call.h @@ -137,9 +137,9 @@ struct Info bool videoMuted = false; // this flag is used to check main video status bool isAudioOnly = false; Layout layout = Layout::GRID; - VectorMapStringString participantsInfos = {}; VectorMapStringString mediaList = {}; QSet<QString> peerRec {}; + bool isConference = false; }; static inline bool diff --git a/src/api/callparticipantsmodel.h b/src/api/callparticipantsmodel.h new file mode 100644 index 0000000000000000000000000000000000000000..9abc14682cea7444782c5b578125967150842e4e --- /dev/null +++ b/src/api/callparticipantsmodel.h @@ -0,0 +1,147 @@ +/*! + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +// std +#include <string> +#include <ctime> +#include <chrono> +#include <mutex> + +// Qt +#include <QObject> +#include <QJsonObject> + +#include "typedefs.h" + +namespace lrc { + +namespace api { +class NewCallModel; + +struct ParticipantInfos +{ + ParticipantInfos() {} + + ParticipantInfos(const MapStringString& infos, const QString& callId, const QString& peerId) + { + uri = infos["uri"]; + if (uri.lastIndexOf("@") > 0) + uri.truncate(uri.lastIndexOf("@")); + if (uri.isEmpty()) + uri = peerId; + device = infos["device"]; + active = infos["active"] == "true"; + x = infos["x"].toInt(); + y = infos["y"].toInt(); + width = infos["w"].toInt(); + height = infos["h"].toInt(); + videoMuted = infos["videoMuted"] == "true"; + audioLocalMuted = infos["audioLocalMuted"] == "true"; + audioModeratorMuted = infos["audioModeratorMuted"] == "true"; + isModerator = infos["isModerator"] == "true"; + handRaised = infos["handRaised"] == "true"; + + if (infos["sinkId"].isEmpty()) + sinkId = callId + uri + device; + else + sinkId = infos["sinkId"]; + + bestName = ""; + } + + QString uri; + QString device; + QString sinkId; + QString bestName; + QString avatar; + bool active {false}; + int x = 0; + int y = 0; + int width = 0; + int height = 0; + bool audioLocalMuted {false}; + bool audioModeratorMuted {false}; + bool videoMuted {false}; + bool isModerator {false}; + bool islocal {false}; + bool isContact {false}; + bool handRaised {false}; + + bool operator==(const ParticipantInfos& other) const + { + return uri == other.uri && sinkId == other.sinkId && active == other.active + && audioLocalMuted == other.audioLocalMuted + && audioModeratorMuted == other.audioModeratorMuted && avatar == other.avatar + && bestName == other.bestName && isContact == other.isContact + && islocal == other.islocal && videoMuted == other.videoMuted + && handRaised == other.handRaised; + } +}; + +class LIB_EXPORT CallParticipants : public QObject +{ + Q_OBJECT + +public: + CallParticipants(const VectorMapStringString& infos, + const QString& callId, + const NewCallModel& linked); + ~CallParticipants() {} + + /** + * @return The list of participants that can have a widget in the client + */ + QList<ParticipantInfos> getParticipants() const; + + /** + * Update the participants + */ + void update(const VectorMapStringString& infos); + + /** + * @param index participant index + * @return informations of the participant in index + */ + QJsonObject toQJsonObject(uint index) const; + +private: + /** + * Filter the participants the might appear for the end user + */ + void filterCandidates(const VectorMapStringString& infos); + + void removeParticipant(int index); + + void addParticipant(const ParticipantInfos& participant); + + // Participants in the conference + QMap<QString, ParticipantInfos> candidates_; + // Participants ordered + QMap<QString, ParticipantInfos> participants_; + QList<QString> validUris_; + int idx_; + + const NewCallModel& linked; + std::mutex streamMtx_ {}; + const QString callId_; +}; +} // end namespace api +} // end namespace lrc +Q_DECLARE_METATYPE(lrc::api::CallParticipants*) diff --git a/src/api/newcallmodel.h b/src/api/newcallmodel.h index 6c786f11486aaaf9b3136033d3e1716d6182de5f..363bdc5f224e27cd7f30c68884b97ed68a044b1a 100644 --- a/src/api/newcallmodel.h +++ b/src/api/newcallmodel.h @@ -50,6 +50,7 @@ struct PendingConferenceeInfo }; } // namespace call class NewAccountModel; +class CallParticipants; /** * @brief Class that manages call informations. @@ -60,6 +61,7 @@ class LIB_EXPORT NewCallModel : public QObject public: using CallInfoMap = std::map<QString, std::shared_ptr<call::Info>>; + using CallParticipantsModelMap = std::map<QString, std::shared_ptr<CallParticipants>>; const account::Info& owner; @@ -105,6 +107,14 @@ public: */ const call::Info& getCall(const QString& uid) const; + /** + * Get the call participantsInfos from its call id + * @param callId + * @return the call participantsInfos + * @throw out_of_range exception if not found + */ + const CallParticipants& getParticipantsInfos(const QString& callId); + /** * Get the call from the peer uri * @param uri @@ -383,6 +393,28 @@ public: */ void setDisplay(int idx, int x, int y, int w, int h, const QString& callId = {}); Q_SIGNALS: + + /** + * Emitted when a participant video is added to a conference + * @param callId + * @param index + */ + void participantAdded(const QString& callId, int index) const; + + /** + * Emitted when a participant video is removed from a conference + * @param callId + * @param index + */ + void participantRemoved(const QString& callId, int index) const; + + /** + * Emitted when, in a conference, participant parameters are changed + * @param callId + * @param index + */ + void participantUpdated(const QString& callId, int index) const; + /** * Emitted when a call state changes * @param callId diff --git a/src/callparticipantsmodel.cpp b/src/callparticipantsmodel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c089011963d29296fe9fa1a3d69fba1abf3c9032 --- /dev/null +++ b/src/callparticipantsmodel.cpp @@ -0,0 +1,151 @@ +/*! + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "api/callparticipantsmodel.h" + +#include "api/account.h" +#include "api/contactmodel.h" +#include "api/contact.h" +#include "api/newcallmodel.h" +#include "api/newaccountmodel.h" + +namespace lrc { + +namespace api { + +CallParticipants::CallParticipants(const VectorMapStringString& infos, + const QString& callId, + const NewCallModel& linked) + : linked(linked) + , callId_(callId) +{ + update(infos); +} + +QList<ParticipantInfos> +CallParticipants::getParticipants() const +{ + return participants_.values(); +} + +void +CallParticipants::update(const VectorMapStringString& infos) +{ + validUris_.clear(); + filterCandidates(infos); + validUris_.sort(); + + idx_ = 0; + auto keys = participants_.keys(); + for (const auto& key : keys) { + auto keyIdx = validUris_.indexOf(key); + if (keyIdx < 0 || keyIdx >= validUris_.size()) + removeParticipant(idx_); + else + idx_++; + } + + idx_ = 0; + for (const auto& partUri : validUris_) { + addParticipant(candidates_[partUri]); + idx_++; + } +} + +void +CallParticipants::removeParticipant(int index) +{ + std::lock_guard<std::mutex> lk(streamMtx_); + auto it = participants_.begin() + index; + participants_.erase(it); + Q_EMIT linked.participantRemoved(callId_, idx_); +} + +void +CallParticipants::addParticipant(const ParticipantInfos& participant) +{ + std::lock_guard<std::mutex> lk(streamMtx_); + auto it = participants_.find(participant.uri); + if (it == participants_.end()) { + participants_.insert(participants_.begin() + idx_, participant.uri, participant); + Q_EMIT linked.participantAdded(callId_, idx_); + } else { + if (participant == (*it)) + return; + (*it) = participant; + Q_EMIT linked.participantUpdated(callId_, idx_); + } +} + +void +CallParticipants::filterCandidates(const VectorMapStringString& infos) +{ + candidates_.clear(); + for (const auto& candidate : infos) { + auto peerId = candidate["uri"]; + peerId.truncate(peerId.lastIndexOf("@")); + if (peerId.isEmpty()) { + for (const auto& accId : linked.owner.accountModel->getAccountList()) { + try { + auto& accountInfo = linked.owner.accountModel->getAccountInfo(accId); + if (accountInfo.callModel->hasCall(callId_)) { + peerId = accountInfo.profileInfo.uri; + } + } catch (...) { + } + } + } + if (candidate["w"].toInt() != 0 && candidate["h"].toInt() != 0) { + validUris_.append(peerId); + candidates_.insert(peerId, ParticipantInfos(candidate, callId_, peerId)); + } + } +} + +QJsonObject +CallParticipants::toQJsonObject(uint index) const +{ + if (index >= participants_.size()) + return {}; + + QJsonObject ret; + const auto& participant = participants_.begin() + index; + + ret["uri"] = participant->uri; + ret["device"] = participant->device; + ret["sinkId"] = participant->sinkId; + ret["bestName"] = participant->bestName; + ret["avatar"] = participant->avatar; + ret["active"] = participant->active; + ret["x"] = participant->x; + ret["y"] = participant->y; + ret["width"] = participant->width; + ret["height"] = participant->height; + ret["audioLocalMuted"] = participant->audioLocalMuted; + ret["audioModeratorMuted"] = participant->audioModeratorMuted; + ret["videoMuted"] = participant->videoMuted; + ret["isModerator"] = participant->isModerator; + ret["islocal"] = participant->islocal; + ret["isContact"] = participant->isContact; + ret["handRaised"] = participant->handRaised; + ret["callId"] = callId_; + + return ret; +} +} // end namespace api +} // end namespace lrc diff --git a/src/newcallmodel.cpp b/src/newcallmodel.cpp index 3bd8cb3ee1b4af421c49110265169500c647b817..6a16d522dce32af7349608aaab9a06e5ba2f85b6 100644 --- a/src/newcallmodel.cpp +++ b/src/newcallmodel.cpp @@ -27,6 +27,7 @@ #include "api/contact.h" #include "api/contactmodel.h" #include "api/pluginmodel.h" +#include "api/callparticipantsmodel.h" #include "api/lrc.h" #include "api/newaccountmodel.h" #include "authority/storagehelper.h" @@ -134,6 +135,7 @@ public: void sendProfile(const QString& callId); NewCallModel::CallInfoMap calls; + NewCallModel::CallParticipantsModelMap participantsModel; const CallbacksHandler& callbacksHandler; const NewCallModel& linked; const BehaviorController& behaviorController; @@ -328,6 +330,17 @@ NewCallModel::getCall(const QString& uid) const return *pimpl_->calls.at(uid); } +const CallParticipants& +NewCallModel::getParticipantsInfos(const QString& callId) +{ + if (pimpl_->participantsModel.find(callId) == pimpl_->participantsModel.end()) { + VectorMapStringString infos = {}; + pimpl_->participantsModel + .emplace(callId, std::make_shared<CallParticipants>(infos, callId, pimpl_->linked)); + } + return *pimpl_->participantsModel.at(callId); +} + void NewCallModel::updateCallMediaList(const QString& callId, bool acceptVideo) { @@ -957,7 +970,7 @@ NewCallModelPimpl::initCallFromDaemon() callInfo->type = call::Type::DIALOG; VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId); - callInfo->participantsInfos = infos; + participantsModel.emplace(callId, std::make_shared<CallParticipants>(infos, callId, linked)); calls.emplace(callId, std::move(callInfo)); // NOTE/BUG: the videorenderer can't know that the client has restarted // So, for now, a user will have to manually restart the medias until @@ -1000,7 +1013,7 @@ NewCallModelPimpl::initConferencesFromDaemon() callInfo->type = call::Type::CONFERENCE; VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId); - callInfo->participantsInfos = infos; + participantsModel.emplace(callId, std::make_shared<CallParticipants>(infos, callId, linked)); calls.emplace(callId, std::move(callInfo)); } } @@ -1105,6 +1118,10 @@ NewCallModel::isModerator(const QString& confId, const QString& uri) auto call = pimpl_->calls.find(confId); if (call == pimpl_->calls.end() or not call->second) return false; + auto participantsModel = pimpl_->participantsModel.find(confId); + if (participantsModel == pimpl_->participantsModel.end() + or participantsModel->second->getParticipants().size() == 0) + return false; auto ownerUri = owner.profileInfo.uri; auto uriToCheck = uri; if (uriToCheck.isEmpty()) { @@ -1113,10 +1130,10 @@ NewCallModel::isModerator(const QString& confId, const QString& uri) auto isModerator = uriToCheck == ownerUri ? call->second->type == lrc::api::call::Type::CONFERENCE : false; - if (!isModerator && call->second->participantsInfos.size() != 0) { - for (const auto& participant : call->second->participantsInfos) { - if (participant["uri"] == uriToCheck) { - isModerator = participant["isModerator"] == "true"; + if (!isModerator && participantsModel->second->getParticipants().size() != 0) { + for (const auto& participant : participantsModel->second->getParticipants()) { + if (participant.uri == uriToCheck) { + isModerator = participant.isModerator; break; } } @@ -1136,17 +1153,20 @@ NewCallModel::isHandRaised(const QString& confId, const QString& uri) noexcept auto call = pimpl_->calls.find(confId); if (call == pimpl_->calls.end() or not call->second) return false; + + auto participantsModel = pimpl_->participantsModel.find(confId); + if (participantsModel == pimpl_->participantsModel.end()) + return false; + auto ownerUri = owner.profileInfo.uri; auto uriToCheck = uri; if (uriToCheck.isEmpty()) { uriToCheck = ownerUri; } auto handRaised = false; - for (const auto& participant : call->second->participantsInfos) { - auto itUri = participant.find("uri"); - auto itHand = participant.find("handRaised"); - if (itUri != participant.end() && itHand != participant.end() && *itUri == uriToCheck) { - handRaised = participant["handRaised"] == "true"; + for (const auto& participant : participantsModel->second->getParticipants()) { + if (participant.uri == uriToCheck) { + handRaised = participant.handRaised; break; } } @@ -1458,17 +1478,19 @@ NewCallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId, if (it == calls.end() or not it->second) return; - qDebug() << "New conference layout received for call " << confId; + if (participantsModel.find(confId) == participantsModel.end()) + participantsModel.emplace(confId, std::make_shared<CallParticipants>(infos, confId, linked)); + else + participantsModel[confId]->update(infos); // if Jami, remove @ring.dht - it->second->participantsInfos = infos; - for (auto& i : it->second->participantsInfos) { - i["uri"].replace("@ring.dht", ""); - if (i["uri"].isEmpty()) { + for (auto& i : participantsModel[confId]->getParticipants()) { + i.uri.replace("@ring.dht", ""); + if (i.uri.isEmpty()) { if (it->second->type == call::Type::CONFERENCE) { - i["uri"] = linked.owner.profileInfo.uri; + i.uri = linked.owner.profileInfo.uri; } else { - i["uri"] = it->second->peerUri.replace("ring:", ""); + i.uri = it->second->peerUri.replace("ring:", ""); } } } @@ -1519,11 +1541,13 @@ NewCallModelPimpl::slotConferenceCreated(const QString& accountId, const QString callInfo->status = call::Status::IN_PROGRESS; callInfo->type = call::Type::CONFERENCE; callInfo->startTime = std::chrono::steady_clock::now(); - callInfo->participantsInfos = CallManager::instance().getConferenceInfos(linked.owner.id, + + VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, confId); - for (auto& i : callInfo->participantsInfos) - i["uri"].replace("@ring.dht", ""); + participantsModel[confId] = std::make_shared<CallParticipants>(infos, confId, linked); + calls[confId] = callInfo; + foreach (const auto& call, callList) { emit linked.callAddedToConference(call, confId); // Remove call from pendingConferences_