/**************************************************************************** * Copyright (C) 2017-2024 Savoir-faire Linux Inc. * * Author : Nicolas Jäger * * Author : Sébastien Blin * * * * 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 . * ***************************************************************************/ #include "api/callmodel.h" // Lrc #include "callbackshandler.h" #include "api/avmodel.h" #include "api/behaviorcontroller.h" #include "api/conversationmodel.h" #include "api/codecmodel.h" #include "api/contact.h" #include "api/contactmodel.h" #include "api/pluginmodel.h" #include "api/callparticipantsmodel.h" #include "api/lrc.h" #include "api/accountmodel.h" #include "authority/storagehelper.h" #include "dbus/callmanager.h" #include "dbus/videomanager.h" #include "vcard.h" #include "renderer.h" #include "typedefs.h" #include "uri.h" // Ring daemon #include #include // Qt #include #include #include #include // std #include #include #include #ifdef WIN32 #define NOMINMAX #include "Windows.h" #endif using namespace libjami::Media; constexpr static const char HARDWARE_ACCELERATION[] = "HARDWARE_ACCELERATION"; constexpr static const char AUDIO_CODEC[] = "AUDIO_CODEC"; constexpr static const char CALL_ID[] = "CALL_ID"; static std::uniform_int_distribution dis {0, std::numeric_limits::max()}; static const std::map sip_call_status_code_map {{0, QObject::tr("Null")}, {100, QObject::tr("Trying")}, {180, QObject::tr("Ringing")}, {181, QObject::tr("Being Forwarded")}, {182, QObject::tr("Queued")}, {183, QObject::tr("Progress")}, {200, QObject::tr("OK")}, {202, QObject::tr("Accepted")}, {300, QObject::tr("Multiple Choices")}, {301, QObject::tr("Moved Permanently")}, {302, QObject::tr("Moved Temporarily")}, {305, QObject::tr("Use Proxy")}, {380, QObject::tr("Alternative Service")}, {400, QObject::tr("Bad Request")}, {401, QObject::tr("Unauthorized")}, {402, QObject::tr("Payment Required")}, {403, QObject::tr("Forbidden")}, {404, QObject::tr("Not Found")}, {405, QObject::tr("Method Not Allowed")}, {406, QObject::tr("Not Acceptable")}, {407, QObject::tr("Proxy Authentication Required")}, {408, QObject::tr("Request Timeout")}, {410, QObject::tr("Gone")}, {413, QObject::tr("Request Entity Too Large")}, {414, QObject::tr("Request URI Too Long")}, {415, QObject::tr("Unsupported Media Type")}, {416, QObject::tr("Unsupported URI Scheme")}, {420, QObject::tr("Bad Extension")}, {421, QObject::tr("Extension Required")}, {422, QObject::tr("Session Timer Too Small")}, {423, QObject::tr("Interval Too Brief")}, {480, QObject::tr("Temporarily Unavailable")}, {481, QObject::tr("Call TSX Does Not Exist")}, {482, QObject::tr("Loop Detected")}, {483, QObject::tr("Too Many Hops")}, {484, QObject::tr("Address Incomplete")}, {485, QObject::tr("Ambiguous")}, {486, QObject::tr("Busy")}, {487, QObject::tr("Request Terminated")}, {488, QObject::tr("Not Acceptable")}, {489, QObject::tr("Bad Event")}, {490, QObject::tr("Request Updated")}, {491, QObject::tr("Request Pending")}, {493, QObject::tr("Undecipherable")}, {500, QObject::tr("Internal Server Error")}, {501, QObject::tr("Not Implemented")}, {502, QObject::tr("Bad Gateway")}, {503, QObject::tr("Service Unavailable")}, {504, QObject::tr("Server Timeout")}, {505, QObject::tr("Version Not Supported")}, {513, QObject::tr("Message Too Large")}, {580, QObject::tr("Precondition Failure")}, {600, QObject::tr("Busy Everywhere")}, {603, QObject::tr("Call Refused")}, {604, QObject::tr("Does Not Exist Anywhere")}, {606, QObject::tr("Not Acceptable Anywhere")}}; namespace lrc { using namespace api; class CallModelPimpl : public QObject { Q_OBJECT public: CallModelPimpl(const CallModel& linked, Lrc& lrc, const CallbacksHandler& callbacksHandler, const BehaviorController& behaviorController); ~CallModelPimpl(); QVariantList callAdvancedInformation(); MapStringString advancedInformationForCallId(QString callId); QStringList getCallIds(); CallModel::CallInfoMap calls; CallModel::CallParticipantsModelMap participantsModel; const CallbacksHandler& callbacksHandler; const CallModel& linked; const BehaviorController& behaviorController; /** * key = peer's uri * vector = chunks * @note chunks are counted from 1 to number of parts. We use 0 to store the actual number of * parts stored */ std::map vcardsChunks; /** * Retrieve active calls from the daemon and init the model */ void initCallFromDaemon(); /** * Retrieve active conferences from the daemon and init the model */ void initConferencesFromDaemon(); /** * Check if media device is muted */ bool checkMediaDeviceMuted(const MapStringString& mediaAttributes); bool manageCurrentCall_ {true}; QString currentCall_ {}; Lrc& lrc; QList pendingConferencees_; QString waitForConference_ {}; public Q_SLOTS: /** * Connect this signal to know when a call arrives * @param accountId the one who receives the call * @param callId the call id * @param mediaList new media received */ void slotMediaChangeRequested(const QString& accountId, const QString& callId, const VectorMapStringString& mediaList); /** * Listen from CallbacksHandler when a call got a new state * @param accountId * @param callId * @param state the new state * @param code unused */ void slotCallStateChanged(const QString& accountId, const QString& callId, const QString& state, int code); /** * Listen from CallbacksHandler when a call medias are ready * @param callId * @param event * @param mediaList */ void slotMediaNegotiationStatus(const QString& callId, const QString& event, const VectorMapStringString& mediaList); /** * Listen from CallbacksHandler when a VCard chunk is incoming * @param accountId * @param callId * @param from * @param part * @param numberOfParts * @param payload */ void slotincomingVCardChunk(const QString& accountId, const QString& callId, const QString& from, int part, int numberOfParts, const QString& payload); /** * Listen from CallbacksHandler when a conference is created. * @param callId */ void slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& callId); void slotConferenceChanged(const QString& accountId, const QString& callId, const QString& state); /** * Listen from CallbacksHandler when a voice mail notice is incoming * @param accountId * @param newCount * @param oldCount * @param urgentCount */ void slotVoiceMailNotify(const QString& accountId, int newCount, int oldCount, int urgentCount); /** * Listen from CallManager when a conference layout is updated * @param confId * @param infos */ void slotOnConferenceInfosUpdated(const QString& confId, const VectorMapStringString& infos); /** * Listen from CallbacksHandler when the peer start recording * @param callId * @param peerUri * @param state the new state */ void onRemoteRecordingChanged(const QString& callId, const QString& peerUri, bool state); /** * Listen from CallbacksHandler when we start/stop recording * @param callId * @param state the new state */ void onRecordingStateChanged(const QString& callId, bool state); }; CallModel::CallModel(const account::Info& owner, Lrc& lrc, const CallbacksHandler& callbacksHandler, const BehaviorController& behaviorController) : QObject(nullptr) , owner(owner) , pimpl_(std::make_unique(*this, lrc, callbacksHandler, behaviorController)) {} CallModel::~CallModel() {} const call::Info& CallModel::getCallFromURI(const QString& uri, bool notOver) const { // For a NON SIP account the scheme can be ring:. Sometimes it can miss, and will be certainly // replaced by jami://. // Just make the comparaison ignoring the scheme and check the rest. auto uriObj = URI(uri); for (const auto& call : pimpl_->calls) { auto contactUri = URI(call.second->peerUri); if (uriObj.userinfo() == contactUri.userinfo() and uriObj.hostname() == contactUri.hostname()) { if (!notOver || !call::isTerminating(call.second->status)) return *call.second; } } throw std::out_of_range("No call at URI " + uri.toStdString()); } const call::Info& CallModel::getConferenceFromURI(const QString& uri) const { for (const auto& call : pimpl_->calls) { if (call.second->type == call::Type::CONFERENCE) { QStringList callList = CallManager::instance().getParticipantList(owner.id, call.first); Q_FOREACH (const auto& callId, callList) { try { if (pimpl_->calls.find(callId) != pimpl_->calls.end() && pimpl_->calls[callId]->peerUri == uri) { return *call.second; } } catch (...) { } } } } throw std::out_of_range("No call at URI " + uri.toStdString()); } VectorString CallModel::getConferenceSubcalls(const QString& confId) { QStringList callList = CallManager::instance().getParticipantList(owner.id, confId); VectorString result; result.reserve(callList.size()); Q_FOREACH (const auto& callId, callList) { result.push_back(callId); } return result; } const call::Info& CallModel::getCall(const QString& uid) const { return *pimpl_->calls.at(uid); } const CallParticipants& CallModel::getParticipantsInfos(const QString& callId) { if (pimpl_->participantsModel.find(callId) == pimpl_->participantsModel.end()) { VectorMapStringString infos = {}; pimpl_->participantsModel .emplace(callId, std::make_shared(infos, callId, pimpl_->linked)); } return *pimpl_->participantsModel.at(callId); } void CallModel::setVideoMuted(const QString& callId, bool videoMuted) { auto call = pimpl_->calls.find(callId); if (call == pimpl_->calls.end()) return; auto& callInfo = call->second; callInfo->videoMuted = videoMuted; for (auto& media : callInfo->mediaList) { if (!media.contains(MediaAttributeKey::MEDIA_TYPE)) continue; if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO) { media[MediaAttributeKey::MUTED] = videoMuted ? TRUE_STR : FALSE_STR; } } } static void initializeMediaList(VectorMapStringString& mediaList, bool audioOnly) { mediaList.push_back({{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::AUDIO}, {MediaAttributeKey::ENABLED, TRUE_STR}, {MediaAttributeKey::MUTED, FALSE_STR}, {MediaAttributeKey::SOURCE, ""}, {MediaAttributeKey::LABEL, "audio_0"}}); if (audioOnly) return; mediaList.push_back({{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO}, {MediaAttributeKey::ENABLED, TRUE_STR}, {MediaAttributeKey::MUTED, FALSE_STR}, {MediaAttributeKey::SOURCE, ""}, {MediaAttributeKey::LABEL, "video_0"}}); } QString CallModel::createCall(const QString& uri, bool isAudioOnly, VectorMapStringString mediaList) { if (mediaList.isEmpty()) { initializeMediaList(mediaList, isAudioOnly); } #ifdef ENABLE_LIBWRAP auto callId = CallManager::instance().placeCallWithMedia(owner.id, uri, mediaList); #else // dbus // do not use auto here (QDBusPendingReply) QString callId = CallManager::instance().placeCallWithMedia(owner.id, uri, mediaList); #endif // ENABLE_LIBWRAP if (callId.isEmpty()) { if (uri.startsWith("swarm:")) { pimpl_->waitForConference_ = uri; return {}; } qWarning() << "no call placed between (account: " << owner.id << ", contact: " << uri << ")"; return ""; } auto callInfo = std::make_shared(); callInfo->id = callId; callInfo->peerUri = uri; callInfo->isOutgoing = true; callInfo->status = call::Status::SEARCHING; callInfo->type = call::Type::DIALOG; callInfo->isAudioOnly = isAudioOnly; callInfo->videoMuted = isAudioOnly; callInfo->mediaList = mediaList; pimpl_->calls.emplace(callId, std::move(callInfo)); return callId; } QList CallModel::getAdvancedInformation() const { return pimpl_->callAdvancedInformation(); } MapStringString CallModel::advancedInformationForCallId(QString callId) const { return pimpl_->advancedInformationForCallId(callId); } QStringList CallModel::getCallIds() const { return pimpl_->getCallIds(); } void CallModel::emplaceConversationConference(const QString& confId) { if (hasCall(confId)) return; auto callInfo = std::make_shared(); callInfo->id = confId; callInfo->isOutgoing = false; callInfo->status = call::Status::SEARCHING; callInfo->type = call::Type::CONFERENCE; callInfo->isAudioOnly = false; callInfo->videoMuted = false; callInfo->mediaList = {}; pimpl_->calls.emplace(confId, std::move(callInfo)); } void CallModel::muteMedia(const QString& callId, const QString& label, bool mute) { auto& callInfo = pimpl_->calls[callId]; if (!callInfo) return; auto proposedList = callInfo->mediaList; if (proposedList.isEmpty()) return; for (auto& media : proposedList) if (media[MediaAttributeKey::LABEL] == label) media[MediaAttributeKey::MUTED] = mute ? TRUE_STR : FALSE_STR; CallManager::instance().requestMediaChange(owner.id, callId, proposedList); } void CallModel::replaceDefaultCamera(const QString& callId, const QString& deviceId) { auto& callInfo = pimpl_->calls[callId]; if (!callInfo) return; VectorMapStringString proposedList = callInfo->mediaList; QString oldPreview, newPreview; for (auto& media : proposedList) { if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO && media[MediaAttributeKey::SOURCE].startsWith( libjami::Media::VideoProtocolPrefix::CAMERA)) { oldPreview = media[MediaAttributeKey::SOURCE]; QString resource = QString("%1%2%3") .arg(libjami::Media::VideoProtocolPrefix::CAMERA) .arg(libjami::Media::VideoProtocolPrefix::SEPARATOR) .arg(deviceId); media[MediaAttributeKey::SOURCE] = resource; newPreview = resource; break; } } if (!newPreview.isEmpty()) { pimpl_->lrc.getAVModel().stopPreview(oldPreview); pimpl_->lrc.getAVModel().startPreview(newPreview); } CallManager::instance().requestMediaChange(owner.id, callId, proposedList); } VectorMapStringString CallModel::getProposed(VectorMapStringString mediaList, const QString& callId, const QString& source, MediaRequestType type, bool mute, bool shareAudio) { QString resource {}; auto aid = 0; auto vid = 0; for (const auto& media : mediaList) { if (media[MediaAttributeKey::SOURCE] == source) break; if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::AUDIO) aid++; if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO) vid++; } QString alabel = QString("audio_%1").arg(aid); QString vlabel = QString("video_%1").arg(vid); QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR; MapStringString audioMediaAttribute {}; switch (type) { case MediaRequestType::FILESHARING: { // File sharing resource = !source.isEmpty() ? QString("%1%2%3") .arg(libjami::Media::VideoProtocolPrefix::FILE) .arg(sep) .arg(QUrl(source).toLocalFile()) : libjami::Media::VideoProtocolPrefix::NONE; if (shareAudio) audioMediaAttribute = {{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::AUDIO}, {MediaAttributeKey::ENABLED, TRUE_STR}, {MediaAttributeKey::MUTED, mute ? TRUE_STR : FALSE_STR}, {MediaAttributeKey::SOURCE, resource}, {MediaAttributeKey::LABEL, alabel}}; break; } case MediaRequestType::SCREENSHARING: { // Screen/window sharing resource = source; break; } case MediaRequestType::CAMERA: { // Camera device resource = not source.isEmpty() ? QString("%1%2%3") .arg(libjami::Media::VideoProtocolPrefix::CAMERA) .arg(sep) .arg(source) : libjami::Media::VideoProtocolPrefix::NONE; break; } default: return mediaList; } VectorMapStringString proposedList {}; MapStringString videoMediaAttribute = {{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO}, {MediaAttributeKey::ENABLED, TRUE_STR}, {MediaAttributeKey::MUTED, mute ? TRUE_STR : FALSE_STR}, {MediaAttributeKey::SOURCE, resource}, {MediaAttributeKey::LABEL, vlabel}}; // if we're in a 1:1, we only show one preview, so, limit to 1 video (the new one) auto participantsModel = pimpl_->participantsModel.find(callId); auto isConf = participantsModel != pimpl_->participantsModel.end() && participantsModel->second->getParticipants().size() != 0; auto replaced = false; for (auto& media : mediaList) { auto replace = media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO; // In a 1:1 we replace the first video, in a conference we replace only if it's a muted // video or if a new sharing is requested if (isConf) { replace &= media[MediaAttributeKey::MUTED] == TRUE_STR; replace |= (media[MediaAttributeKey::SOURCE].startsWith( libjami::Media::VideoProtocolPrefix::FILE) || media[MediaAttributeKey::SOURCE].startsWith( libjami::Media::VideoProtocolPrefix::DISPLAY)) && (type == MediaRequestType::FILESHARING || type == MediaRequestType::SCREENSHARING); } if (replace) { videoMediaAttribute[MediaAttributeKey::LABEL] = media[MediaAttributeKey::LABEL]; media = videoMediaAttribute; replaced = true; } if (!(media[MediaAttributeKey::SOURCE].startsWith(libjami::Media::VideoProtocolPrefix::FILE) && type == MediaRequestType::CAMERA)) { proposedList.emplace_back(media); } } if (!replaced) proposedList.push_back(videoMediaAttribute); if (!audioMediaAttribute.isEmpty()) proposedList.emplace_back(audioMediaAttribute); return proposedList; } void CallModel::addMedia( const QString& callId, const QString& source, MediaRequestType type, bool mute, bool shareAudio) { auto& callInfo = pimpl_->calls[callId]; if (!callInfo || source.isEmpty()) return; auto proposedList = getProposed(callInfo->mediaList, callId, source, type, mute, shareAudio); CallManager::instance().requestMediaChange(owner.id, callId, proposedList); callInfo->mediaList = proposedList; if (callInfo->status == call::Status::IN_PROGRESS) Q_EMIT callInfosChanged(owner.id, callId); } void CallModel::removeMedia(const QString& callId, const QString& mediaType, const QString& type, bool muteCamera, bool removeAll) { auto& callInfo = pimpl_->calls[callId]; if (!callInfo) return; auto isVideo = mediaType == MediaAttributeValue::VIDEO; auto newIdx = 0; auto replaceIdx = false, hasVideo = false; VectorMapStringString proposedList; QString label; for (const auto& media : callInfo->mediaList) { if (media[MediaAttributeKey::MEDIA_TYPE] == mediaType && media[MediaAttributeKey::SOURCE].startsWith(type)) { replaceIdx = true; label = media[MediaAttributeKey::LABEL]; } else { if (!removeAll || !media[MediaAttributeKey::SOURCE].startsWith(type)) { if (media[MediaAttributeKey::MEDIA_TYPE] == mediaType) { auto newMedia = media; if (replaceIdx) { QString idxStr = QString::number(newIdx); newMedia[MediaAttributeKey::LABEL] = isVideo ? "video_" + idxStr : "audio_" + idxStr; } proposedList.push_back(newMedia); newIdx++; } else { proposedList.push_back(media); } } hasVideo |= media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO; } } auto participantsModel = pimpl_->participantsModel.find(callId); auto isConf = participantsModel != pimpl_->participantsModel.end() && participantsModel->second->getParticipants().size() != 0; if (!isConf) { // 1:1 call, in this case we only show one preview, and switch between sharing and camera // preview So, if no video, replace by camera if (!hasVideo) { proposedList = getProposed(proposedList, callInfo->id, pimpl_->lrc.getAVModel().getCurrentVideoCaptureDevice(), MediaRequestType::CAMERA, muteCamera); } } else if (!hasVideo) { // To receive the remote video, we need a muted camera proposedList.push_back(MapStringString { {MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO}, {MediaAttributeKey::ENABLED, TRUE_STR}, {MediaAttributeKey::MUTED, TRUE_STR}, {MediaAttributeKey::SOURCE, pimpl_->lrc.getAVModel() .getCurrentVideoCaptureDevice()}, // not needed to set the source. Daemon should be // able to check it {MediaAttributeKey::LABEL, label.isEmpty() ? "video_0" : label}}); } if (isVideo && !label.isEmpty()) pimpl_->lrc.getAVModel().stopPreview(label); CallManager::instance().requestMediaChange(owner.id, callId, proposedList); callInfo->mediaList = proposedList; if (callInfo->status == call::Status::IN_PROGRESS) Q_EMIT callInfosChanged(owner.id, callId); } void CallModel::accept(const QString& callId) const { try { auto& callInfo = pimpl_->calls[callId]; if (!callInfo) return; if (callInfo->mediaList.empty()) CallManager::instance().accept(owner.id, callId); else CallManager::instance().acceptWithMedia(owner.id, callId, callInfo->mediaList); } catch (...) { } } void CallModel::hangUp(const QString& callId) const { if (!hasCall(callId)) return; auto& call = pimpl_->calls[callId]; if (call->status == call::Status::INCOMING_RINGING) { CallManager::instance().refuse(owner.id, callId); return; } switch (call->type) { case call::Type::DIALOG: CallManager::instance().hangUp(owner.id, callId); break; case call::Type::CONFERENCE: CallManager::instance().hangUpConference(owner.id, callId); break; case call::Type::INVALID: default: break; } } void CallModel::refuse(const QString& callId) const { if (!hasCall(callId)) return; CallManager::instance().refuse(owner.id, callId); } void CallModel::toggleAudioRecord(const QString& callId) const { CallManager::instance().toggleRecording(owner.id, callId); } void CallModel::playDTMF(const QString& callId, const QString& value) const { if (!hasCall(callId)) return; if (pimpl_->calls[callId]->status != call::Status::IN_PROGRESS) return; CallManager::instance().playDTMF(value); } void CallModel::togglePause(const QString& callId) const { // function should now only serves for SIP accounts if (!hasCall(callId)) return; auto& call = pimpl_->calls[callId]; if (call->status == call::Status::PAUSED) { if (call->type == call::Type::DIALOG) { CallManager::instance().unhold(owner.id, callId); } else { CallManager::instance().unholdConference(owner.id, callId); } } else if (call->status == call::Status::IN_PROGRESS) { if (call->type == call::Type::DIALOG) CallManager::instance().hold(owner.id, callId); else { CallManager::instance().holdConference(owner.id, callId); } } } void CallModel::setQuality(const QString& callId, const double quality) const { Q_UNUSED(callId) Q_UNUSED(quality) qDebug() << "setQuality isn't implemented yet"; } void CallModel::transfer(const QString& callId, const QString& to) const { CallManager::instance().transfer(owner.id, callId, to); } void CallModel::transferToCall(const QString& callId, const QString& callIdDest) const { CallManager::instance().attendedTransfer(owner.id, callId, callIdDest); } void CallModel::joinCalls(const QString& callIdA, const QString& callIdB) const { // Get call informations call::Info call1, call2; QString accountIdCall1 = {}, accountIdCall2 = {}; for (const auto& account_id : owner.accountModel->getAccountList()) { try { auto& accountInfo = owner.accountModel->getAccountInfo(account_id); if (accountInfo.callModel->hasCall(callIdA)) { call1 = accountInfo.callModel->getCall(callIdA); accountIdCall1 = account_id; } if (accountInfo.callModel->hasCall(callIdB)) { call2 = accountInfo.callModel->getCall(callIdB); accountIdCall2 = account_id; } if (!accountIdCall1.isEmpty() && !accountIdCall2.isEmpty()) break; } catch (...) { } } if (accountIdCall1.isEmpty() || accountIdCall2.isEmpty()) { qWarning() << "Can't join inexistent calls."; return; } if (call1.type == call::Type::CONFERENCE && call2.type == call::Type::CONFERENCE) { bool joined = CallManager::instance().joinConference(accountIdCall1, callIdA, accountIdCall2, callIdB); if (!joined) { qWarning() << "Conference: " << callIdA << " couldn't join conference " << callIdB; return; } if (accountIdCall1 != owner.id) { // If the conference is added from another account try { auto& accountInfo = owner.accountModel->getAccountInfo(accountIdCall1); if (accountInfo.callModel->hasCall(callIdA)) { Q_EMIT accountInfo.callModel->callAddedToConference(callIdA, "", callIdB); } } catch (...) { } } else { Q_EMIT callAddedToConference(callIdA, "", callIdB); } } else if (call1.type == call::Type::CONFERENCE || call2.type == call::Type::CONFERENCE) { auto call = call1.type == call::Type::CONFERENCE ? callIdB : callIdA; auto conf = call1.type == call::Type::CONFERENCE ? callIdA : callIdB; // Unpause conference if conference was not active CallManager::instance().unholdConference(owner.id, conf); auto accountCall = call1.type == call::Type::CONFERENCE ? accountIdCall2 : accountIdCall1; bool joined = CallManager::instance().addParticipant(accountCall, call, accountCall, conf); if (!joined) { qWarning() << "Call: " << call << " couldn't join conference " << conf; return; } if (accountCall != owner.id) { // If the call is added from another account try { auto& accountInfo = owner.accountModel->getAccountInfo(accountCall); if (accountInfo.callModel->hasCall(call)) { accountInfo.callModel->pimpl_->slotConferenceCreated(owner.id, "", conf); } } catch (...) { } } else Q_EMIT callAddedToConference(call, "", conf); // Remove from pendingConferences_ for (int i = 0; i < pimpl_->pendingConferencees_.size(); ++i) { if (pimpl_->pendingConferencees_.at(i).callId == call) { Q_EMIT beginRemovePendingConferenceesRows(i); pimpl_->pendingConferencees_.removeAt(i); Q_EMIT endRemovePendingConferenceesRows(); break; } } } else { CallManager::instance().joinParticipant(accountIdCall1, callIdA, accountIdCall2, callIdB); // NOTE: This will trigger slotConferenceCreated. } } QString CallModel::callAndAddParticipant(const QString uri, const QString& callId, bool audioOnly) { auto newCallId = createCall(uri, audioOnly, pimpl_->calls[callId]->mediaList); Q_EMIT beginInsertPendingConferenceesRows(0); pimpl_->pendingConferencees_.prepend({uri, newCallId, callId}); Q_EMIT endInsertPendingConferenceesRows(); return newCallId; } void CallModel::removeParticipant(const QString& callId, const QString& participant) const { Q_UNUSED(callId) Q_UNUSED(participant) qDebug() << "removeParticipant() isn't implemented yet"; } QString CallModel::getFormattedCallDuration(const QString& callId) const { if (!hasCall(callId)) return "00:00"; auto& startTime = pimpl_->calls[callId]->startTime; if (startTime.time_since_epoch().count() == 0) return "00:00"; auto now = std::chrono::steady_clock::now(); auto d = std::chrono::duration_cast(now.time_since_epoch() - startTime.time_since_epoch()) .count(); return interaction::getFormattedCallDuration(d); } bool CallModel::isRecording(const QString& callId) const { if (!hasCall(callId)) return false; return CallManager::instance().getIsRecording(owner.id, callId); } QString CallModel::getSIPCallStatusString(const short& statusCode) { auto element = sip_call_status_code_map.find(statusCode); if (element != sip_call_status_code_map.end()) { return element->second; } return ""; } const QList& CallModel::getPendingConferencees() { return pimpl_->pendingConferencees_; } api::video::RenderedDevice CallModel::getCurrentRenderedDevice(const QString& call_id) const { video::RenderedDevice result; MapStringString callDetails; QStringList conferences = CallManager::instance().getConferenceList(owner.id); if (conferences.indexOf(call_id) != -1) { callDetails = CallManager::instance().getConferenceDetails(owner.id, call_id); } else { callDetails = CallManager::instance().getCallDetails(owner.id, call_id); } if (!callDetails.contains("VIDEO_SOURCE")) { return result; } auto source = callDetails["VIDEO_SOURCE"]; auto sourceSize = source.size(); if (source.startsWith("camera://")) { result.type = video::DeviceType::CAMERA; result.name = source.right(sourceSize - QString("camera://").size()); } else if (source.startsWith("file://")) { result.type = video::DeviceType::FILE; result.name = source.right(sourceSize - QString("file://").size()); } else if (source.startsWith("display://")) { result.type = video::DeviceType::DISPLAY; result.name = source.right(sourceSize - QString("display://").size()); } return result; } QString CallModel::getDisplay(int idx, int x, int y, int w, int h) { QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR; return QString("%1%2:%3+%4,%5 %6x%7") .arg(libjami::Media::VideoProtocolPrefix::DISPLAY) .arg(sep) .arg(idx) .arg(x) .arg(y) .arg(w) .arg(h); } QString CallModel::getDisplay(const QString& windowProcessId, const QString& windowId) { #if defined(__APPLE__) Q_UNUSED(windowProcessId) Q_UNUSED(windowId) return {}; #else QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR; QString ret {}; #if defined(Q_OS_UNIX) Q_UNUSED(windowId); ret = QString("%1%2:+0,0 window-id:%3") .arg(libjami::Media::VideoProtocolPrefix::DISPLAY) .arg(sep) .arg(windowProcessId); #elif WIN32 ret = QString("%1%2:+0,0 window-id:hwnd=%3") .arg(libjami::Media::VideoProtocolPrefix::DISPLAY) .arg(sep) .arg(windowProcessId); #endif return ret; #endif } CallModelPimpl::CallModelPimpl(const CallModel& linked, Lrc& lrc, const CallbacksHandler& callbacksHandler, const BehaviorController& behaviorController) : linked(linked) , lrc(lrc) , callbacksHandler(callbacksHandler) , behaviorController(behaviorController) { connect(&callbacksHandler, &CallbacksHandler::mediaChangeRequested, this, &CallModelPimpl::slotMediaChangeRequested); connect(&callbacksHandler, &CallbacksHandler::callStateChanged, this, &CallModelPimpl::slotCallStateChanged); connect(&callbacksHandler, &CallbacksHandler::mediaNegotiationStatus, this, &CallModelPimpl::slotMediaNegotiationStatus); connect(&callbacksHandler, &CallbacksHandler::incomingVCardChunk, this, &CallModelPimpl::slotincomingVCardChunk); connect(&callbacksHandler, &CallbacksHandler::conferenceCreated, this, &CallModelPimpl::slotConferenceCreated); connect(&callbacksHandler, &CallbacksHandler::conferenceChanged, this, &CallModelPimpl::slotConferenceChanged); connect(&callbacksHandler, &CallbacksHandler::voiceMailNotify, this, &CallModelPimpl::slotVoiceMailNotify); connect(&CallManager::instance(), &CallManagerInterface::onConferenceInfosUpdated, this, &CallModelPimpl::slotOnConferenceInfosUpdated); connect(&callbacksHandler, &CallbacksHandler::remoteRecordingChanged, this, &CallModelPimpl::onRemoteRecordingChanged); connect(&callbacksHandler, &CallbacksHandler::recordingStateChanged, this, &CallModelPimpl::onRecordingStateChanged); #ifndef ENABLE_LIBWRAP // Only necessary with dbus since the daemon runs separately initCallFromDaemon(); initConferencesFromDaemon(); #endif } CallModelPimpl::~CallModelPimpl() {} QVariantList CallModelPimpl::callAdvancedInformation() { QVariantList advancedInformationList; QStringList callList = CallManager::instance().getCallList(linked.owner.id); for (const auto& callId : callList) { MapStringString mapStringDetailsList = CallManager::instance() .getCallDetails(linked.owner.id, callId); QVariantMap detailsList = mapStringStringToQVariantMap(mapStringDetailsList); detailsList.insert(CALL_ID, callId); detailsList.insert(HARDWARE_ACCELERATION, lrc.getAVModel().getHardwareAcceleration()); advancedInformationList.append(detailsList); } return advancedInformationList; } MapStringString CallModelPimpl::advancedInformationForCallId(QString callId) { MapStringString infoMap = CallManager::instance().getCallDetails(linked.owner.id, callId); if (lrc.getAVModel().getHardwareAcceleration()) infoMap[HARDWARE_ACCELERATION] = "True"; else infoMap[HARDWARE_ACCELERATION] = "False"; return infoMap; } QStringList CallModelPimpl::getCallIds() { return CallManager::instance().getCallList(linked.owner.id); } void CallModelPimpl::initCallFromDaemon() { QStringList callList = CallManager::instance().getCallList(linked.owner.id); for (const auto& callId : callList) { MapStringString details = CallManager::instance().getCallDetails(linked.owner.id, callId); auto callInfo = std::make_shared(); callInfo->id = callId; auto now = std::chrono::steady_clock::now(); auto system_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); auto diff = static_cast(system_now) - std::stol(details["TIMESTAMP_START"].toStdString()); callInfo->startTime = now - std::chrono::seconds(diff); callInfo->status = call::to_status(details["CALL_STATE"]); auto endId = details["PEER_NUMBER"].indexOf("@"); callInfo->peerUri = details["PEER_NUMBER"].left(endId); if (linked.owner.profileInfo.type == lrc::api::profile::Type::JAMI) { callInfo->peerUri = "ring:" + callInfo->peerUri; } callInfo->videoMuted = details["VIDEO_MUTED"] == TRUE_STR; callInfo->audioMuted = details["AUDIO_MUTED"] == TRUE_STR; callInfo->type = call::Type::DIALOG; VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId); auto participantsPtr = std::make_shared(infos, callId, linked); callInfo->layout = participantsPtr->getLayout(); participantsModel.emplace(callId, std::move(participantsPtr)); 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 // this renderer is not redesigned. } } bool CallModelPimpl::checkMediaDeviceMuted(const MapStringString& mediaAttributes) { return mediaAttributes[MediaAttributeKey::SOURCE].startsWith("camera:") && (mediaAttributes[MediaAttributeKey::ENABLED] == FALSE_STR || mediaAttributes[MediaAttributeKey::MUTED] == TRUE_STR); } void CallModelPimpl::initConferencesFromDaemon() { QStringList callList = CallManager::instance().getConferenceList(linked.owner.id); for (const auto& callId : callList) { QMap details = CallManager::instance() .getConferenceDetails(linked.owner.id, callId); auto callInfo = std::make_shared(); callInfo->id = callId; QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, callId); Q_FOREACH (const auto& call, callList) { MapStringString callDetails = CallManager::instance().getCallDetails(linked.owner.id, call); auto now = std::chrono::steady_clock::now(); auto system_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); auto diff = static_cast(system_now) - std::stol(callDetails["TIMESTAMP_START"].toStdString()); callInfo->status = details["CONF_STATE"] == "ACTIVE_ATTACHED" ? call::Status::IN_PROGRESS : call::Status::PAUSED; callInfo->startTime = now - std::chrono::seconds(diff); Q_EMIT linked.callAddedToConference(call, "", callId); } callInfo->type = call::Type::CONFERENCE; VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId); auto participantsPtr = std::make_shared(infos, callId, linked); callInfo->layout = participantsPtr->getLayout(); participantsModel.emplace(callId, std::move(participantsPtr)); calls.emplace(callId, std::move(callInfo)); } } void CallModel::setCurrentCall(const QString& callId) const { if (!pimpl_->manageCurrentCall_) return; auto it = std::find_if(pimpl_->pendingConferencees_.begin(), pimpl_->pendingConferencees_.end(), [callId](const lrc::api::call::PendingConferenceeInfo& info) -> bool { return info.callId == callId; }); // Set current call only if not adding this call // to a current conference if (it != pimpl_->pendingConferencees_.end()) return; if (!hasCall(callId)) return; // The client should be able to set the current call multiple times if (pimpl_->currentCall_ == callId) return; pimpl_->currentCall_ = callId; // Unhold call auto& call = pimpl_->calls[callId]; if (call->status == call::Status::PAUSED) { auto& call = pimpl_->calls[callId]; if (call->type == call::Type::DIALOG) { CallManager::instance().unhold(owner.id, callId); } else { CallManager::instance().unholdConference(owner.id, callId); } } QStringList accountList = pimpl_->lrc.getAccountModel().getAccountList(); // If we are setting a current call in the UI, we want to hold all other calls, // across accounts, to avoid sending our local media streams while another call // is in focus. for (const auto& acc : accountList) { VectorString filterCalls; // For each account, we should not hold calls linked to a conference QStringList conferences = CallManager::instance().getConferenceList(acc); for (const auto& confId : conferences) { QStringList callList = CallManager::instance().getParticipantList(acc, confId); Q_FOREACH (const auto& cid, callList) { filterCalls.push_back(cid); } } for (const auto& cid : Lrc::activeCalls(acc)) { auto filtered = std::find(filterCalls.begin(), filterCalls.end(), cid) != filterCalls.end(); if (cid != callId && !filtered) { // Only hold calls for a non rendez-vous point CallManager::instance().hold(acc, cid); } } if (!lrc::api::Lrc::holdConferences) { continue; } // If the account is the host and it is attached to the conference, // then we should hold it. for (const auto& confId : conferences) { if (callId != confId) { MapStringString confDetails = CallManager::instance().getConferenceDetails(acc, confId); // Only hold conference if attached if (confDetails["CALL_STATE"] == "ACTIVE_DETACHED") continue; QStringList callList = CallManager::instance().getParticipantList(acc, confId); if (callList.indexOf(callId) == -1) CallManager::instance().holdConference(acc, confId); } } } Q_EMIT currentCallChanged(callId); } void CallModel::setConferenceLayout(const QString& confId, const call::Layout& layout) { auto call = pimpl_->calls.find(confId); if (call != pimpl_->calls.end()) { switch (layout) { case call::Layout::GRID: CallManager::instance().setConferenceLayout(owner.id, confId, 0); break; case call::Layout::ONE_WITH_SMALL: CallManager::instance().setConferenceLayout(owner.id, confId, 1); break; case call::Layout::ONE: CallManager::instance().setConferenceLayout(owner.id, confId, 2); break; } call->second->layout = layout; } } void CallModel::setActiveStream(const QString& confId, const QString& accountUri, const QString& deviceId, const QString& streamId, bool state) { CallManager::instance().setActiveStream(owner.id, confId, accountUri, deviceId, streamId, state); } bool CallModel::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 true; auto ownerUri = owner.profileInfo.uri; auto uriToCheck = uri; if (uriToCheck.isEmpty()) { uriToCheck = ownerUri; } auto isModerator = uriToCheck == ownerUri ? call->second->type == lrc::api::call::Type::CONFERENCE : false; if (!isModerator && participantsModel->second->getParticipants().size() != 0) { if (!uri.isEmpty()) isModerator = participantsModel->second->checkModerator(uri); else isModerator = participantsModel->second->checkModerator(owner.profileInfo.uri); } return isModerator; } void CallModel::setModerator(const QString& confId, const QString& peerId, const bool& state) { CallManager::instance().setModerator(owner.id, confId, peerId, state); } bool CallModel::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 : participantsModel->second->getParticipants()) { if (participant.uri == uriToCheck) { handRaised = participant.handRaised; break; } } return handRaised; } void CallModel::raiseHand(const QString& confId, const QString& accountUri, const QString& deviceId, bool state) { CallManager::instance().raiseHand(owner.id, confId, accountUri, deviceId, state); } void CallModel::muteStream(const QString& confId, const QString& accountUri, const QString& deviceId, const QString& streamId, const bool& state) { CallManager::instance().muteStream(owner.id, confId, accountUri, deviceId, streamId, state); } void CallModel::hangupParticipant(const QString& confId, const QString& accountUri, const QString& deviceId) { CallManager::instance().hangupParticipant(owner.id, confId, accountUri, deviceId); } void CallModel::sendSipMessage(const QString& callId, const QString& body) const { MapStringString payloads; payloads[TEXT_PLAIN] = body; CallManager::instance().sendTextMessage(owner.id, callId, payloads, true /* not used */); } bool CallModel::isConferenceHost(const QString& callId) { auto call = pimpl_->calls.find(callId); if (call == pimpl_->calls.end() or not call->second) return false; else return call->second->type == lrc::api::call::Type::CONFERENCE; } void CallModelPimpl::slotMediaChangeRequested(const QString& accountId, const QString& callId, const VectorMapStringString& mediaList) { if (linked.owner.id != accountId) { return; } if (mediaList.empty()) return; auto& callInfo = calls[callId]; if (!callInfo) return; QList currentMediaLabels {}; for (auto& currentItem : callInfo->mediaList) currentMediaLabels.append(currentItem[MediaAttributeKey::LABEL]); auto answerMedia = QList::fromVector(mediaList); for (auto& item : answerMedia) { int index = currentMediaLabels.indexOf(item[MediaAttributeKey::LABEL]); if (index >= 0) { item[MediaAttributeKey::MUTED] = callInfo->mediaList[index][MediaAttributeKey::MUTED]; item[MediaAttributeKey::ENABLED] = callInfo->mediaList[index][MediaAttributeKey::ENABLED]; } else { item[MediaAttributeKey::MUTED] = TRUE_STR; item[MediaAttributeKey::ENABLED] = TRUE_STR; } } CallManager::instance().answerMediaChangeRequest(linked.owner.id, callId, QVector::fromList( answerMedia)); } void CallModelPimpl::slotCallStateChanged(const QString& accountId, const QString& callId, const QString& state, int code) { if (accountId != linked.owner.id) return; if (!linked.hasCall(callId)) { auto callInfo = std::make_shared(); callInfo->id = callId; MapStringString details = CallManager::instance().getCallDetails(linked.owner.id, callId); qDebug() << details; auto endId = details["PEER_NUMBER"].indexOf("@"); callInfo->peerUri = details["PEER_NUMBER"].left(endId); callInfo->isOutgoing = details["CALL_TYPE"] == "1"; callInfo->status = call::to_status(state); callInfo->type = call::Type::DIALOG; callInfo->isAudioOnly = details["AUDIO_ONLY"] == TRUE_STR; callInfo->videoMuted = details["VIDEO_MUTED"] == TRUE_STR; // NOTE: The CallModel::setVideoMuted function currently relies on callInfo->mediaList // having been initialized. Not doing so leads to a bug where the user's camera wrongly // gets turned on when they receive a call and click on "Answer in audio". initializeMediaList(callInfo->mediaList, callInfo->isAudioOnly); calls.emplace(callId, std::move(callInfo)); if (!(details["CALL_TYPE"] == "1") && !linked.owner.confProperties.allowIncoming && linked.owner.profileInfo.type == profile::Type::JAMI) { linked.refuse(callId); return; } QString displayname = details["DISPLAY_NAME"]; QString peerId; QString peerUri = details["PEER_NUMBER"]; if (peerUri.contains("ring.dht")) { peerId = peerUri.right(50); peerId = peerId.left(40); if (displayname.isEmpty()) displayname = details["REGISTERED_NAME"]; } else { auto left = std::max(peerUri.indexOf("<"), peerUri.indexOf(":")) + 1; auto right = peerUri.indexOf("@"); right = std::max(right, peerUri.indexOf(">")); peerId = peerUri.mid(left, right - left); if (displayname.isEmpty()) displayname = peerId; } qDebug() << displayname; qDebug() << peerId; Q_EMIT linked.newCall(peerId, callId, displayname, details["CALL_TYPE"] == "1", details["TO_USERNAME"]); // NOTE: signal emission order matters, always emit CallStatusChanged before CallEnded Q_EMIT linked.callStatusChanged(callId, code); Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId); } auto status = call::to_status(state); auto& call = calls[callId]; if (!call) return; if (status == call::Status::ENDED && !call::isTerminating(call->status)) { call->status = call::Status::TERMINATING; Q_EMIT linked.callStatusChanged(callId, code); Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId); } // proper state transition auto previousStatus = call->status; call->status = status; if (previousStatus == call->status) { // call state didn't change, simply ignore signal return; } qDebug() << QString("slotCallStateChanged (call: %1), from %2 to %3") .arg(callId) .arg(call::to_string(previousStatus)) .arg(call::to_string(status)); // NOTE: signal emission order matters, always emit CallStatusChanged before CallEnded Q_EMIT linked.callStatusChanged(callId, code); Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId); if (call->status == call::Status::ENDED) { Q_EMIT linked.callEnded(callId); // Remove from pendingConferences_ for (int i = 0; i < pendingConferencees_.size(); ++i) { if (pendingConferencees_.at(i).callId == callId) { Q_EMIT linked.beginRemovePendingConferenceesRows(i); pendingConferencees_.removeAt(i); Q_EMIT linked.endRemovePendingConferenceesRows(); break; } } } else if (call->status == call::Status::IN_PROGRESS) { if (previousStatus == call::Status::INCOMING_RINGING || previousStatus == call::Status::OUTGOING_RINGING) { call->startTime = std::chrono::steady_clock::now(); Q_EMIT linked.callStarted(callId); } // Add to calls if in pendingConferences_ for (int i = 0; i < pendingConferencees_.size(); ++i) { if (pendingConferencees_.at(i).callId == callId) { linked.joinCalls(pendingConferencees_.at(i).callIdToJoin, pendingConferencees_.at(i).callId); break; } } } else if (call->status == call::Status::PAUSED) { currentCall_ = ""; } } void CallModelPimpl::slotMediaNegotiationStatus(const QString& callId, const QString&, const VectorMapStringString& mediaList) { if (!linked.hasCall(callId)) { return; } auto& callInfo = calls[callId]; if (!callInfo) { return; } callInfo->isAudioOnly = true; callInfo->videoMuted = true; for (const auto& item : mediaList) { if (item[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO) { if (item[MediaAttributeKey::ENABLED] == TRUE_STR) { callInfo->isAudioOnly = false; } callInfo->videoMuted = checkMediaDeviceMuted(item); } if (item[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::AUDIO) { callInfo->audioMuted = checkMediaDeviceMuted(item); } } callInfo->mediaList = mediaList; if (callInfo->status == call::Status::IN_PROGRESS) Q_EMIT linked.callInfosChanged(linked.owner.id, callId); } void CallModelPimpl::slotincomingVCardChunk(const QString& accountId, const QString& callId, const QString& from, int part, int numberOfParts, const QString& payload) { if (accountId != linked.owner.id || !linked.hasCall(callId)) return; auto it = vcardsChunks.find(from); if (it != vcardsChunks.end()) { vcardsChunks[from][part - 1] = payload; if (not std::any_of(vcardsChunks[from].begin(), vcardsChunks[from].end(), [](const auto& s) { return s.isEmpty(); })) { profile::Info profileInfo; profileInfo.uri = from; profileInfo.type = profile::Type::JAMI; QString vcardPhoto; for (auto& chunk : vcardsChunks[from]) vcardPhoto += chunk; for (auto& e : QString(vcardPhoto).split("\n")) if (e.contains("PHOTO")) profileInfo.avatar = e.split(":")[1]; else if (e.contains("FN")) profileInfo.alias = e.split(":")[1]; contact::Info contactInfo; contactInfo.profileInfo = profileInfo; linked.owner.contactModel->addContact(contactInfo); contactInfo.profileInfo.avatar.clear(); // Do not want avatar in memory here vcardsChunks.erase(from); // Transfer is finish, we don't want to reuse this entry. } } else { vcardsChunks[from] = VectorString(numberOfParts); vcardsChunks[from][part - 1] = payload; } } void CallModelPimpl::slotVoiceMailNotify(const QString& accountId, int newCount, int oldCount, int urgentCount) { Q_EMIT linked.voiceMailNotify(accountId, newCount, oldCount, urgentCount); } void CallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId, const VectorMapStringString& infos) { auto it = calls.find(confId); if (it == calls.end() or not it->second) return; // TODO: remove when the rendez-vous UI will be done // For now, the rendez-vous account can see ongoing calls // And must be notified when a new QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); Q_FOREACH (const auto& call, callList) { Q_EMIT linked.callAddedToConference(call, "", confId); if (calls.find(call) == calls.end()) { qWarning() << "Call not found"; } else { calls[call]->videoMuted = it->second->videoMuted; calls[call]->audioMuted = it->second->audioMuted; Q_EMIT linked.callInfosChanged(linked.owner.id, call); } } auto participantIt = participantsModel.find(confId); if (participantIt == participantsModel.end()) participantIt = participantsModel .emplace(confId, std::make_shared(infos, confId, linked)) .first; else participantIt->second->update(infos); it->second->layout = participantIt->second->getLayout(); // if Jami, remove @ring.dht for (auto& i : participantIt->second->getParticipants()) { i.uri.replace("@ring.dht", ""); if (i.uri.isEmpty()) { if (it->second->type == call::Type::CONFERENCE) { i.uri = linked.owner.profileInfo.uri; } else { i.uri = it->second->peerUri.replace("ring:", ""); } } } for (auto& info : infos) { if (info["uri"].isEmpty()) { it->second->videoMuted = info["videoMuted"] == TRUE_STR; it->second->audioMuted = info["audioLocalMuted"] == TRUE_STR; } } Q_EMIT linked.callInfosChanged(linked.owner.id, confId); Q_EMIT linked.participantsChanged(confId); } bool CallModel::hasCall(const QString& callId) const { return pimpl_->calls.find(callId) != pimpl_->calls.end(); } void CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& confId) { if (accountId != linked.owner.id) return; auto callInfo = std::make_shared(); callInfo->id = confId; callInfo->status = call::Status::IN_PROGRESS; callInfo->type = call::Type::CONFERENCE; callInfo->startTime = std::chrono::steady_clock::now(); VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, confId); auto participantsPtr = std::make_shared(infos, confId, linked); callInfo->layout = participantsPtr->getLayout(); VectorMapStringString mediaList = CallManager::instance().currentMediaList(linked.owner.id, confId); callInfo->mediaList = mediaList; participantsModel[confId] = participantsPtr; calls[confId] = callInfo; QString currentCallId = currentCall_; if (!conversationId.isEmpty()) { Q_EMIT linked.callAddedToConference("", conversationId, confId); if (currentCall_ != confId && waitForConference_.contains(conversationId)) { currentCall_ = confId; Q_EMIT linked.currentCallChanged(confId); } } else { QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); Q_FOREACH (const auto& call, callList) { Q_EMIT linked.callAddedToConference(call, "", confId); // Remove call from pendingConferences_ for (int i = 0; i < pendingConferencees_.size(); ++i) { if (pendingConferencees_.at(i).callId == call) { Q_EMIT linked.beginRemovePendingConferenceesRows(i); pendingConferencees_.removeAt(i); Q_EMIT linked.endRemovePendingConferenceesRows(); break; } } if (call == currentCall_) currentCall_ = confId; } if (currentCallId != currentCall_) Q_EMIT linked.currentCallChanged(confId); } } void CallModelPimpl::slotConferenceChanged(const QString& accountId, const QString& confId, const QString&) { if (accountId != linked.owner.id) return; // Detect if conference is created for this account QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); QString currentCallId = currentCall_; Q_FOREACH (const auto& call, callList) { Q_EMIT linked.callAddedToConference(call, "", confId); if (call == currentCall_) currentCall_ = confId; } Q_EMIT linked.currentCallChanged(currentCall_); } void CallModelPimpl::onRemoteRecordingChanged(const QString& callId, const QString& peerUri, bool state) { auto it = calls.find(callId); if (it == calls.end() or !it->second) { return; } auto uri = peerUri; if (uri.contains("ring:")) uri.remove("ring:"); if (uri.contains("jami:")) uri.remove("jami:"); if (uri.contains("@ring.dht")) uri.remove("@ring.dht"); // Add/remove peer to recordingPeers, preventing duplicates. if (state && !it->second->recordingPeers.contains(uri)) it->second->recordingPeers.append(uri); else if (!state && it->second->recordingPeers.contains(uri)) it->second->recordingPeers.removeAll(uri); Q_EMIT linked.remoteRecordersChanged(callId, it->second->recordingPeers); } void CallModelPimpl::onRecordingStateChanged(const QString& callId, bool state) { Q_EMIT linked.recordingStateChanged(callId, state); } } // namespace lrc #include "api/moc_callmodel.cpp" #include "callmodel.moc"