/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "CallModel.hpp"
#include
#include
#include
#include
#include "app/App.hpp"
#include "components/calls/CallsListModel.hpp"
#include "components/core/CoreHandlers.hpp"
#include "components/core/CoreManager.hpp"
#include "components/notifier/Notifier.hpp"
#include "components/settings/AccountSettingsModel.hpp"
#include "components/settings/SettingsModel.hpp"
#include "utils/LinphoneUtils.hpp"
#include "utils/MediastreamerUtils.hpp"
#include "utils/Utils.hpp"
// =============================================================================
using namespace std;
namespace {
constexpr char AutoAnswerObjectName[] = "auto-answer-timer";
constexpr int DtmfSoundDelay = 200;
}
CallModel::CallModel (shared_ptr call) {
Q_CHECK_PTR(call);
mCall = call;
mCall->setData("call-model", *this);
updateIsInConference();
CoreManager *coreManager = CoreManager::getInstance();
// Deal with auto-answer.
if (!isOutgoing()) {
SettingsModel *settings = coreManager->getSettingsModel();
if (settings->getAutoAnswerStatus()) {
QTimer *timer = new QTimer(this);
timer->setInterval(settings->getAutoAnswerDelay());
timer->setSingleShot(true);
timer->setObjectName(AutoAnswerObjectName);
QObject::connect(timer, &QTimer::timeout, this, &CallModel::acceptWithAutoAnswerDelay);
timer->start();
}
}
CoreHandlers *coreHandlers = coreManager->getHandlers().get();
QObject::connect(
coreHandlers, &CoreHandlers::callStateChanged,
this, &CallModel::handleCallStateChanged
);
QObject::connect(
coreHandlers, &CoreHandlers::callEncryptionChanged,
this, &CallModel::handleCallEncryptionChanged
);
}
CallModel::~CallModel () {
mCall->unsetData("call-model");
}
// -----------------------------------------------------------------------------
QString CallModel::getPeerAddress () const {
return Utils::coreStringToAppString(mCall->getRemoteAddress()->asStringUriOnly());
}
QString CallModel::getLocalAddress () const {
return Utils::coreStringToAppString(mCall->getCallLog()->getLocalAddress()->asStringUriOnly());
}
// -----------------------------------------------------------------------------
void CallModel::setRecordFile (const shared_ptr &callParams) {
callParams->setRecordFile(Utils::appStringToCoreString(
CoreManager::getInstance()->getSettingsModel()->getSavedCallsFolder()
.append(generateSavedFilename())
.append(".mkv")
));
}
void CallModel::setRecordFile (const shared_ptr &callParams, const QString &to) {
const QString from(
Utils::coreStringToAppString(
CoreManager::getInstance()->getAccountSettingsModel()->getUsedSipAddress()->getUsername()
)
);
callParams->setRecordFile(Utils::appStringToCoreString(
CoreManager::getInstance()->getSettingsModel()->getSavedCallsFolder()
.append(generateSavedFilename(from, to))
.append(".mkv")
));
}
// -----------------------------------------------------------------------------
void CallModel::updateStats (const shared_ptr &callStats) {
switch (callStats->getType()) {
case linphone::StreamType::Text:
case linphone::StreamType::Unknown:
break;
case linphone::StreamType::Audio:
updateStats(callStats, mAudioStats);
break;
case linphone::StreamType::Video:
updateStats(callStats, mVideoStats);
break;
}
emit statsUpdated();
}
// -----------------------------------------------------------------------------
float CallModel::getSpeakerVolumeGain () const {
return mCall->getSpeakerVolumeGain();
}
void CallModel::setSpeakerVolumeGain (float volume) {
Q_ASSERT(volume >= 0.0f && volume <= 1.0f);
mCall->setSpeakerVolumeGain(volume);
emit speakerVolumeGainChanged(getSpeakerVolumeGain());
}
float CallModel::getMicroVolumeGain () const {
return mCall->getMicrophoneVolumeGain();
}
void CallModel::setMicroVolumeGain (float volume) {
Q_ASSERT(volume >= 0.0f && volume <= 1.0f);
mCall->setMicrophoneVolumeGain(volume);
emit microVolumeGainChanged(getMicroVolumeGain());
}
// -----------------------------------------------------------------------------
void CallModel::notifyCameraFirstFrameReceived (unsigned int width, unsigned int height) {
if (mNotifyCameraFirstFrameReceived) {
mNotifyCameraFirstFrameReceived = false;
emit cameraFirstFrameReceived(width, height);
}
}
// -----------------------------------------------------------------------------
void CallModel::accept () {
accept(false);
}
void CallModel::acceptWithVideo () {
accept(true);
}
void CallModel::terminate () {
CoreManager *core = CoreManager::getInstance();
core->lockVideoRender();
mCall->terminate();
core->unlockVideoRender();
}
// -----------------------------------------------------------------------------
void CallModel::askForTransfer () {
CoreManager::getInstance()->getCallsListModel()->askForTransfer(this);
}
bool CallModel::transferTo (const QString &sipAddress) {
bool status = !!mCall->transfer(Utils::appStringToCoreString(sipAddress));
if (status)
qWarning() << QStringLiteral("Unable to transfer: `%1`.").arg(sipAddress);
return status;
}
// -----------------------------------------------------------------------------
void CallModel::acceptVideoRequest () {
shared_ptr core = CoreManager::getInstance()->getCore();
shared_ptr params = core->createCallParams(mCall);
params->enableVideo(true);
mCall->acceptUpdate(params);
}
void CallModel::rejectVideoRequest () {
mCall->acceptUpdate(mCall->getCurrentParams());
}
void CallModel::takeSnapshot () {
static QString oldName;
QString newName(generateSavedFilename().append(".jpg"));
if (newName == oldName) {
qWarning() << QStringLiteral("Unable to take snapshot. Wait one second.");
return;
}
oldName = newName;
qInfo() << QStringLiteral("Take snapshot of call:") << this;
const QString filePath(CoreManager::getInstance()->getSettingsModel()->getSavedScreenshotsFolder().append(newName));
mCall->takeVideoSnapshot(Utils::appStringToCoreString(filePath));
App::getInstance()->getNotifier()->notifySnapshotWasTaken(filePath);
}
void CallModel::startRecording () {
if (mRecording)
return;
qInfo() << QStringLiteral("Start recording call:") << this;
mCall->startRecording();
mRecording = true;
emit recordingChanged(true);
}
void CallModel::stopRecording () {
if (!mRecording)
return;
qInfo() << QStringLiteral("Stop recording call:") << this;
mRecording = false;
mCall->stopRecording();
App::getInstance()->getNotifier()->notifyRecordingCompleted(
Utils::coreStringToAppString(mCall->getParams()->getRecordFile())
);
emit recordingChanged(false);
}
// -----------------------------------------------------------------------------
void CallModel::handleCallEncryptionChanged (const shared_ptr &call) {
if (call == mCall)
emit securityUpdated();
}
void CallModel::handleCallStateChanged (const shared_ptr &call, linphone::Call::State state) {
if (call != mCall)
return;
updateIsInConference();
switch (state) {
case linphone::Call::State::Error:
case linphone::Call::State::End:
setCallErrorFromReason(call->getReason());
stopAutoAnswerTimer();
stopRecording();
mPausedByRemote = false;
break;
case linphone::Call::State::StreamsRunning: {
if (!mWasConnected && CoreManager::getInstance()->getSettingsModel()->getAutomaticallyRecordCalls()) {
startRecording();
mWasConnected = true;
}
mPausedByRemote = false;
break;
}
case linphone::Call::State::Connected:
case linphone::Call::State::Referred:
case linphone::Call::State::Released:
mPausedByRemote = false;
break;
case linphone::Call::State::PausedByRemote:
mNotifyCameraFirstFrameReceived = true;
mPausedByRemote = true;
break;
case linphone::Call::State::Pausing:
mNotifyCameraFirstFrameReceived = true;
mPausedByUser = true;
break;
case linphone::Call::State::Resuming:
mPausedByUser = false;
break;
case linphone::Call::State::UpdatedByRemote:
if (!mCall->getCurrentParams()->videoEnabled() && mCall->getRemoteParams()->videoEnabled()) {
mCall->deferUpdate();
emit videoRequested();
}
break;
case linphone::Call::State::Idle:
case linphone::Call::State::IncomingReceived:
case linphone::Call::State::OutgoingInit:
case linphone::Call::State::OutgoingProgress:
case linphone::Call::State::OutgoingRinging:
case linphone::Call::State::OutgoingEarlyMedia:
case linphone::Call::State::Paused:
case linphone::Call::State::IncomingEarlyMedia:
case linphone::Call::State::Updating:
case linphone::Call::State::EarlyUpdatedByRemote:
case linphone::Call::State::EarlyUpdating:
break;
}
emit statusChanged(getStatus());
}
// -----------------------------------------------------------------------------
void CallModel::accept (bool withVideo) {
stopAutoAnswerTimer();
CoreManager *coreManager = CoreManager::getInstance();
shared_ptr core = coreManager->getCore();
shared_ptr params = core->createCallParams(mCall);
params->enableVideo(withVideo);
setRecordFile(params);
QQuickWindow *callsWindow = App::getInstance()->getCallsWindow();
if (callsWindow) {
if (coreManager->getSettingsModel()->getKeepCallsWindowInBackground()) {
if (!callsWindow->isVisible())
callsWindow->showMinimized();
} else
App::smartShowWindow(callsWindow);
}
mCall->acceptWithParams(params);
}
// -----------------------------------------------------------------------------
void CallModel::updateIsInConference () {
if (mIsInConference != mCall->getParams()->getLocalConferenceMode()) {
mIsInConference = !mIsInConference;
emit isInConferenceChanged(mIsInConference);
}
}
// -----------------------------------------------------------------------------
void CallModel::stopAutoAnswerTimer () const {
QTimer *timer = findChild(AutoAnswerObjectName, Qt::FindDirectChildrenOnly);
if (timer) {
timer->stop();
timer->deleteLater();
}
}
// -----------------------------------------------------------------------------
CallModel::CallStatus CallModel::getStatus () const {
switch (mCall->getState()) {
case linphone::Call::State::Connected:
case linphone::Call::State::StreamsRunning:
return CallStatusConnected;
case linphone::Call::State::End:
case linphone::Call::State::Error:
case linphone::Call::State::Referred:
case linphone::Call::State::Released:
return CallStatusEnded;
case linphone::Call::State::Paused:
case linphone::Call::State::PausedByRemote:
case linphone::Call::State::Pausing:
case linphone::Call::State::Resuming:
return CallStatusPaused;
case linphone::Call::State::Updating:
case linphone::Call::State::UpdatedByRemote:
return mPausedByRemote ? CallStatusPaused : CallStatusConnected;
case linphone::Call::State::EarlyUpdatedByRemote:
case linphone::Call::State::EarlyUpdating:
case linphone::Call::State::Idle:
case linphone::Call::State::IncomingEarlyMedia:
case linphone::Call::State::IncomingReceived:
case linphone::Call::State::OutgoingEarlyMedia:
case linphone::Call::State::OutgoingInit:
case linphone::Call::State::OutgoingProgress:
case linphone::Call::State::OutgoingRinging:
break;
}
return mCall->getDir() == linphone::Call::Dir::Incoming ? CallStatusIncoming : CallStatusOutgoing;
}
// -----------------------------------------------------------------------------
void CallModel::acceptWithAutoAnswerDelay () {
CoreManager *coreManager = CoreManager::getInstance();
SettingsModel *settingsModel = coreManager->getSettingsModel();
// Use auto-answer if activated and it's the only call.
if (settingsModel->getAutoAnswerStatus() && coreManager->getCore()->getCallsNb() == 1) {
if (mCall->getRemoteParams()->videoEnabled() && settingsModel->getAutoAnswerVideoStatus() && settingsModel->getVideoSupported())
acceptWithVideo();
else
accept();
}
}
// -----------------------------------------------------------------------------
QString CallModel::getCallError () const {
return mCallError;
}
void CallModel::setCallErrorFromReason (linphone::Reason reason) {
switch (reason) {
case linphone::Reason::Declined:
mCallError = tr("callErrorDeclined");
break;
case linphone::Reason::NotFound:
mCallError = tr("callErrorNotFound");
break;
case linphone::Reason::Busy:
mCallError = tr("callErrorBusy");
break;
case linphone::Reason::NotAcceptable:
mCallError = tr("callErrorNotAcceptable");
break;
default:
break;
}
if (!mCallError.isEmpty())
qInfo() << QStringLiteral("Call terminated with error (%1):").arg(mCallError) << this;
emit callErrorChanged(mCallError);
}
// -----------------------------------------------------------------------------
int CallModel::getDuration () const {
return mCall->getDuration();
}
float CallModel::getQuality () const {
return mCall->getCurrentQuality();
}
// -----------------------------------------------------------------------------
float CallModel::getSpeakerVu () const {
return MediastreamerUtils::computeVu(mCall->getPlayVolume());
}
float CallModel::getMicroVu () const {
return MediastreamerUtils::computeVu(mCall->getRecordVolume());
}
// -----------------------------------------------------------------------------
bool CallModel::getSpeakerMuted () const {
return mCall->getSpeakerMuted();
}
void CallModel::setSpeakerMuted (bool status) {
if (status == getSpeakerMuted())
return;
mCall->setSpeakerMuted(status);
emit speakerMutedChanged(getSpeakerMuted());
}
// -----------------------------------------------------------------------------
bool CallModel::getMicroMuted () const {
return mCall->getMicrophoneMuted();
}
void CallModel::setMicroMuted (bool status) {
if (status == getMicroMuted())
return;
mCall->setMicrophoneMuted(status);
emit microMutedChanged(getMicroMuted());
}
// -----------------------------------------------------------------------------
bool CallModel::getPausedByUser () const {
return mPausedByUser;
}
void CallModel::setPausedByUser (bool status) {
switch (mCall->getState()) {
case linphone::Call::State::Connected:
case linphone::Call::State::StreamsRunning:
case linphone::Call::State::Paused:
case linphone::Call::State::PausedByRemote:
break;
default: return;
}
if (status) {
if (!mPausedByUser)
mCall->pause();
return;
}
if (mPausedByUser)
mCall->resume();
}
// -----------------------------------------------------------------------------
bool CallModel::getVideoEnabled () const {
shared_ptr params = mCall->getCurrentParams();
return params && params->videoEnabled() && getStatus() == CallStatusConnected;
}
void CallModel::setVideoEnabled (bool status) {
shared_ptr core = CoreManager::getInstance()->getCore();
if (!core->videoSupported()) {
qWarning() << QStringLiteral("Unable to update video call property. (Video not supported.)");
return;
}
switch (mCall->getState()) {
case linphone::Call::State::Connected:
case linphone::Call::State::StreamsRunning:
break;
default: return;
}
if (status == getVideoEnabled())
return;
shared_ptr params = core->createCallParams(mCall);
params->enableVideo(status);
mCall->update(params);
}
// -----------------------------------------------------------------------------
bool CallModel::getUpdating () const {
switch (mCall->getState()) {
case linphone::Call::State::Connected:
case linphone::Call::State::StreamsRunning:
case linphone::Call::State::Paused:
case linphone::Call::State::PausedByRemote:
return false;
default:
break;
}
return true;
}
bool CallModel::getRecording () const {
return mRecording;
}
// -----------------------------------------------------------------------------
void CallModel::sendDtmf (const QString &dtmf) {
const char key = dtmf.constData()[0].toLatin1();
qInfo() << QStringLiteral("Send dtmf: `%1`.").arg(key);
mCall->sendDtmf(key);
CoreManager::getInstance()->getCore()->playDtmf(key, DtmfSoundDelay);
}
// -----------------------------------------------------------------------------
void CallModel::verifyAuthenticationToken (bool verify) {
mCall->setAuthenticationTokenVerified(verify);
emit securityUpdated();
}
// -----------------------------------------------------------------------------
void CallModel::updateStreams () {
mCall->update(nullptr);
}
// -----------------------------------------------------------------------------
CallModel::CallEncryption CallModel::getEncryption () const {
return static_cast(mCall->getCurrentParams()->getMediaEncryption());
}
bool CallModel::isSecured () const {
shared_ptr params = mCall->getCurrentParams();
linphone::MediaEncryption encryption = params->getMediaEncryption();
return (
encryption == linphone::MediaEncryption::ZRTP && mCall->getAuthenticationTokenVerified()
) || encryption == linphone::MediaEncryption::SRTP || encryption == linphone::MediaEncryption::DTLS;
}
// -----------------------------------------------------------------------------
QString CallModel::getLocalSas () const {
QString token = Utils::coreStringToAppString(mCall->getAuthenticationToken());
return mCall->getDir() == linphone::Call::Dir::Incoming ? token.left(2).toUpper() : token.right(2).toUpper();
}
QString CallModel::getRemoteSas () const {
QString token = Utils::coreStringToAppString(mCall->getAuthenticationToken());
return mCall->getDir() != linphone::Call::Dir::Incoming ? token.left(2).toUpper() : token.right(2).toUpper();
}
// -----------------------------------------------------------------------------
QString CallModel::getSecuredString () const {
switch (mCall->getCurrentParams()->getMediaEncryption()) {
case linphone::MediaEncryption::SRTP:
return QStringLiteral("SRTP");
case linphone::MediaEncryption::ZRTP:
return QStringLiteral("ZRTP");
case linphone::MediaEncryption::DTLS:
return QStringLiteral("DTLS");
case linphone::MediaEncryption::None:
break;
}
return QString("");
}
// -----------------------------------------------------------------------------
QVariantList CallModel::getAudioStats () const {
return mAudioStats;
}
QVariantList CallModel::getVideoStats () const {
return mVideoStats;
}
// -----------------------------------------------------------------------------
static inline QVariantMap createStat (const QString &key, const QString &value) {
QVariantMap m;
m["key"] = key;
m["value"] = value;
return m;
}
void CallModel::updateStats (const shared_ptr &callStats, QVariantList &statsList) {
shared_ptr params = mCall->getCurrentParams();
shared_ptr payloadType;
switch (callStats->getType()) {
case linphone::StreamType::Audio:
payloadType = params->getUsedAudioPayloadType();
break;
case linphone::StreamType::Video:
payloadType = params->getUsedVideoPayloadType();
break;
default:
return;
}
QString family;
switch (callStats->getIpFamilyOfRemote()) {
case linphone::AddressFamily::Inet:
family = QStringLiteral("IPv4");
break;
case linphone::AddressFamily::Inet6:
family = QStringLiteral("IPv6");
break;
default:
family = QStringLiteral("Unknown");
break;
}
statsList.clear();
statsList << createStat(tr("callStatsCodec"), payloadType
? QStringLiteral("%1 / %2kHz").arg(Utils::coreStringToAppString(payloadType->getMimeType())).arg(payloadType->getClockRate() / 1000)
: QString(""));
statsList << createStat(tr("callStatsUploadBandwidth"), QStringLiteral("%1 kbits/s").arg(int(callStats->getUploadBandwidth())));
statsList << createStat(tr("callStatsDownloadBandwidth"), QStringLiteral("%1 kbits/s").arg(int(callStats->getDownloadBandwidth())));
statsList << createStat(tr("callStatsIceState"), iceStateToString(callStats->getIceState()));
statsList << createStat(tr("callStatsIpFamily"), family);
statsList << createStat(tr("callStatsSenderLossRate"), QStringLiteral("%1 %").arg(static_cast(callStats->getSenderLossRate())));
statsList << createStat(tr("callStatsReceiverLossRate"), QStringLiteral("%1 %").arg(static_cast(callStats->getReceiverLossRate())));
switch (callStats->getType()) {
case linphone::StreamType::Audio:
statsList << createStat(tr("callStatsJitterBuffer"), QStringLiteral("%1 ms").arg(callStats->getJitterBufferSizeMs()));
break;
case linphone::StreamType::Video: {
statsList << createStat(tr("callStatsEstimatedDownloadBandwidth"), QStringLiteral("%1 kbits/s").arg(int(callStats->getEstimatedDownloadBandwidth())));
const QString sentVideoDefinitionName = Utils::coreStringToAppString(params->getSentVideoDefinition()->getName());
const QString sentVideoDefinition = QStringLiteral("%1x%2")
.arg(params->getSentVideoDefinition()->getWidth())
.arg(params->getSentVideoDefinition()->getHeight());
statsList << createStat(tr("callStatsSentVideoDefinition"), sentVideoDefinition == sentVideoDefinitionName
? sentVideoDefinition
: QStringLiteral("%1 (%2)").arg(sentVideoDefinition).arg(sentVideoDefinitionName));
const QString receivedVideoDefinitionName = Utils::coreStringToAppString(params->getReceivedVideoDefinition()->getName());
const QString receivedVideoDefinition = QString("%1x%2")
.arg(params->getReceivedVideoDefinition()->getWidth())
.arg(params->getReceivedVideoDefinition()->getHeight());
statsList << createStat(tr("callStatsReceivedVideoDefinition"), receivedVideoDefinition == receivedVideoDefinitionName
? receivedVideoDefinition
: QString("%1 (%2)").arg(receivedVideoDefinition).arg(receivedVideoDefinitionName));
statsList << createStat(tr("callStatsReceivedFramerate"), QStringLiteral("%1 FPS").arg(static_cast(params->getReceivedFramerate())));
statsList << createStat(tr("callStatsSentFramerate"), QStringLiteral("%1 FPS").arg(static_cast(params->getSentFramerate())));
} break;
default:
break;
}
}
// -----------------------------------------------------------------------------
QString CallModel::iceStateToString (linphone::IceState state) const {
switch (state) {
case linphone::IceState::NotActivated:
return tr("iceStateNotActivated");
case linphone::IceState::Failed:
return tr("iceStateFailed");
case linphone::IceState::InProgress:
return tr("iceStateInProgress");
case linphone::IceState::ReflexiveConnection:
return tr("iceStateReflexiveConnection");
case linphone::IceState::HostConnection:
return tr("iceStateHostConnection");
case linphone::IceState::RelayConnection:
return tr("iceStateRelayConnection");
}
return tr("iceStateInvalid");
}
// -----------------------------------------------------------------------------
QString CallModel::generateSavedFilename () const {
const shared_ptr callLog(mCall->getCallLog());
return generateSavedFilename(
Utils::coreStringToAppString(callLog->getFromAddress()->getUsername()),
Utils::coreStringToAppString(callLog->getToAddress()->getUsername())
);
}
QString CallModel::generateSavedFilename (const QString &from, const QString &to) {
auto escape = [](const QString &str) {
constexpr char ReservedCharacters[] = "<>:\"/\\|\\?\\*";
static QRegularExpression regexp(ReservedCharacters);
return QString(str).replace(regexp, "");
};
return QStringLiteral("%1_%2_%3")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss"))
.arg(escape(from))
.arg(escape(to));
}