/* * 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)); }