blob: 373c0f3a50caeb1f8fa34ca8aa58bd3cee67be51 [file] [log] [blame]
/****************************************************************************
* Copyright (C) 2017-2024 Savoir-faire Linux Inc. *
* Author : Nicolas Jäger <nicolas.jager@savoirfairelinux.com> *
* Author : Sébastien Blin <sebastien.blin@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/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 <media_const.h>
#include <account_const.h>
// Qt
#include <QObject>
#include <QString>
#include <QUrl>
#include <QSize>
// std
#include <chrono>
#include <random>
#include <map>
#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<int> dis {0, std::numeric_limits<int>::max()};
static const std::map<short, QString>
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();
/**
* Send the profile VCard into a call
* @param callId
*/
void sendProfile(const QString& callId);
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<QString, VectorString> 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<call::PendingConferenceeInfo> 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<CallModelPimpl>(*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<CallParticipants>(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>)
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<call::Info>();
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<QVariant>
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<call::Info>();
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<std::chrono::seconds>(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<call::PendingConferenceeInfo>&
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<call::Info>();
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<int64_t>(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<CallParticipants>(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<QString, QString> details = CallManager::instance()
.getConferenceDetails(linked.owner.id, callId);
auto callInfo = std::make_shared<call::Info>();
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<int64_t>(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<CallParticipants>(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<QString> currentMediaLabels {};
for (auto& currentItem : callInfo->mediaList)
currentMediaLabels.append(currentItem[MediaAttributeKey::LABEL]);
auto answerMedia = QList<MapStringString>::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<MapStringString>::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<call::Info>();
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);
sendProfile(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<CallParticipants>(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<call::Info>();
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<CallParticipants>(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::sendProfile(const QString& callId)
{
auto vCard = linked.owner.accountModel->accountVCard(linked.owner.id);
std::random_device rdev;
auto key = std::to_string(dis(rdev));
int i = 0;
int total = vCard.size() / 1000 + (vCard.size() % 1000 ? 1 : 0);
while (vCard.size()) {
auto sizeLimit = std::min(1000, static_cast<int>(vCard.size()));
MapStringString chunk;
chunk[QString("%1; id=%2,part=%3,of=%4")
.arg(lrc::vCard::PROFILE_VCF)
.arg(key.c_str())
.arg(QString::number(i + 1))
.arg(QString::number(total))]
= vCard.left(sizeLimit);
vCard.remove(0, sizeLimit);
++i;
CallManager::instance().sendTextMessage(linked.owner.id, callId, chunk, false);
}
}
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"