diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 703eba2a..9190d99c 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -8,7 +8,6 @@ #include "SIPCall.h" #include "SIPAccount.h" -#include "UserInfo.h" #include "IMHandler.h" #include "ViewHelper.h" @@ -19,13 +18,14 @@ IMHandler::IMHandler(SIPCall *parent) : QObject(parent), m_call(parent) ReadOnlyConfdSettings settings; if (settings.contains("jitsi/url")) { - settings.beginGroup("jitsi"); m_jitsiBaseURL = settings.value("url", "").toString(); m_jitsiPreconfig = settings.value("preconfig", false).toBool(); settings.endGroup(); } + + m_dtmfDebugConfigured = settings.value("generic/dtmfDebug", false).toBool(); } bool IMHandler::process(const QString &contentType, const QString &message) @@ -88,6 +88,33 @@ bool IMHandler::process(const QString &contentType, const QString &message) return true; } + // Call delays + else if (path == "callDelay" && dtmfDebugEnabled()) { + bool validTimestamp = false; + qint64 timestamp = 0; + QString digit; + QUrlQuery q(callUrl); + auto qitems = q.queryItems(); + + for (auto &qi : std::as_const(qitems)) { + if (qi.first == "timestamp") { + timestamp = qi.second.toLongLong(&validTimestamp); + } + if (qi.first == "digit") { + digit = qi.second; + } + } + + if (!validTimestamp) { + qCWarning(lcIMHandler) << "invalid call delay timestamp received"; + return false; + } + + m_call->setCallDelayTx(timestamp, digit); + + return true; + } + return false; } @@ -153,6 +180,9 @@ bool IMHandler::sendCapabilities() m_ownCapabilities.push_back("jitsi"); } + q.addQueryItem("callDelay", "1"); + m_ownCapabilities.push_back("callDelay"); + QUrl jitsiUrl("gonnect:"); jitsiUrl.setPath("capabilities"); jitsiUrl.setQuery(q); diff --git a/src/sip/IMHandler.h b/src/sip/IMHandler.h index 5d3799e3..3048428d 100644 --- a/src/sip/IMHandler.h +++ b/src/sip/IMHandler.h @@ -18,6 +18,11 @@ class IMHandler : public QObject { return !m_jitsiBaseURL.isEmpty() && m_capabilities.contains("jitsi"); } + bool dtmfDebugEnabled() const + { + return m_dtmfDebugConfigured && m_capabilities.contains("callDelay"); + } + bool process(const QString &contentType, const QString &message); bool capabilitiesSent() const { return m_capabilitiesSent; } @@ -53,4 +58,5 @@ class IMHandler : public QObject bool m_capabilitiesSent = false; bool m_jitsiPreconfig = false; + bool m_dtmfDebugConfigured = false; }; diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index a9f662ac..80b6ca66 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "SIPCall.h" #include "SIPCallManager.h" @@ -72,6 +74,21 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool globalCallState.holdAllCalls(this); } } + + m_callDelayCycleTimer.setInterval(10s); + connect(&m_callDelayCycleTimer, &QTimer::timeout, this, [this]() { + QString digit = QString("%1").arg((m_callDelayCounter % 9) + 1); + m_callDelayCounter++; + + requestCallDelay(digit); + }); + + connect(this, &SIPCall::capabilitiesChanged, [this]() { + if (m_imHandler->dtmfDebugEnabled() && isEstablished() + && !m_callDelayCycleTimer.isActive()) { + m_callDelayCycleTimer.start(); + } + }); } SIPCall::~SIPCall() @@ -265,6 +282,10 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) m_isEstablished = false; m_earlyCallState = false; + if (m_imHandler->dtmfDebugEnabled() && m_callDelayCycleTimer.isActive()) { + m_callDelayCycleTimer.stop(); + } + break; default: @@ -348,16 +369,10 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) qCWarning(lcSIPCall) << "failed to send message:" << prm.code << prm.reason; } -pj::AudioMedia *SIPCall::audioMedia() const +void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) { - const auto callInfo = getInfo(); - - for (unsigned i = 0; i < callInfo.media.size(); ++i) { - if (callInfo.media[i].type == PJMEDIA_TYPE_AUDIO) { - return static_cast(getMedia(i)); - } - } - return nullptr; + // INFO: Currently only used for DTMF call latency debugging + setCallDelayRx(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); } void SIPCall::onCallTsxState(pj::OnCallTsxStateParam &prm) @@ -377,6 +392,18 @@ void SIPCall::onCallTsxState(pj::OnCallTsxStateParam &prm) } } +pj::AudioMedia *SIPCall::audioMedia() const +{ + const auto callInfo = getInfo(); + + for (unsigned i = 0; i < callInfo.media.size(); ++i) { + if (callInfo.media[i].type == PJMEDIA_TYPE_AUDIO) { + return static_cast(getMedia(i)); + } + } + return nullptr; +} + bool SIPCall::hold() { pj::CallOpParam op(true); @@ -646,3 +673,50 @@ void SIPCall::addMetadata(const QString &data) Q_EMIT metadataChanged(); } + +void SIPCall::requestCallDelay(QString digit) +{ + // DMTF + QString timestamp = QString("%1").arg(QDateTime::currentMSecsSinceEpoch()); + auto &cm = SIPCallManager::instance(); + cm.sendDtmf(m_account->id(), getId(), digit); + + // IM + pj::SendInstantMessageParam prm; + prm.contentType = "application/x-www-form-urlencoded"; + + QUrlQuery q; + q.addQueryItem("timestamp", timestamp); + q.addQueryItem("digit", digit); + + QUrl delayUrl("gonnect:"); + delayUrl.setPath("callDelay"); + delayUrl.setQuery(q); + prm.content = delayUrl.toString().toStdString(); + + try { + sendInstantMessage(prm); + } catch (pj::Error &err) { + qCWarning(lcSIPCall) << "failed to send call delay data:" << err.info(); + } +} + +void SIPCall::setCallDelayTx(qint64 timestamp, QString digit) +{ + m_callDelayTx.first = timestamp; + m_callDelayTx.second = digit; +} + +void SIPCall::setCallDelayRx(qint64 timestamp, QString digit) +{ + m_callDelayRx.first = timestamp; + m_callDelayRx.second = digit; + + if (m_callDelayRx.second != m_callDelayTx.second) { + m_callDelay = -1; + } else { + m_callDelay = m_callDelayRx.first - m_callDelayTx.first; + } + + Q_EMIT callDelayChanged(); +} diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index 88c0b187..29592517 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include @@ -28,6 +29,7 @@ class SIPCall : public ICallState, public pj::Call virtual void onCallMediaState(pj::OnCallMediaStateParam &prm) override; virtual void onInstantMessage(pj::OnInstantMessageParam &prm) override; virtual void onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) override; + virtual void onDtmfDigit(pj::OnDtmfDigitParam &prm) override; virtual void onCallTsxState(pj::OnCallTsxStateParam &prm) override; SIPAccount *account() const { return m_account; }; @@ -50,6 +52,11 @@ class SIPCall : public ICallState, public pj::Call bool hasMetadata() const { return m_hasMetadata; } QList metadata() { return m_metadata; }; + void requestCallDelay(QString digit); + + void setCallDelayTx(qint64 timestamp, QString digit); + void setCallDelayRx(qint64 timestamp, QString digit); + bool hold(); bool unhold(); bool isHolding() const { return m_isHolding; } @@ -64,6 +71,8 @@ class SIPCall : public ICallState, public pj::Call /// The time when the call was established (i.e. answered); invalid QDateTime if not established QDateTime establishedTime() const { return m_establishedTime; } + int callDelay() const { return m_callDelay; } + bool earlyCallState() const { return m_earlyCallState; } virtual ContactInfo remoteContactInfo() const override { return m_contactInfo; } @@ -81,6 +90,7 @@ class SIPCall : public ICallState, public pj::Call void capabilitiesChanged(); void contactChanged(); void metadataChanged(); + void callDelayChanged(); private Q_SLOTS: void updateIsBlocked(); @@ -99,6 +109,12 @@ private Q_SLOTS: QList m_metadata; + QTimer m_callDelayCycleTimer; + QPair m_callDelayTx; + QPair m_callDelayRx; + int m_callDelayCounter = 0; + int m_callDelay = -1; + pj::AudioMedia *m_aud_med = NULL; IMHandler *m_imHandler = nullptr; diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index afd64550..33cd10a4 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -687,6 +687,8 @@ void SIPCallManager::addCall(SIPCall *call) [call, this]() { Q_EMIT callContactChanged(call); }); connect(call, &SIPCall::metadataChanged, this, [call, this]() { Q_EMIT metadataChanged(call); }); + connect(call, &SIPCall::callDelayChanged, this, + [call, this]() { Q_EMIT callDelayChanged(call); }); connect(call, &SIPCall::missed, this, [this, call]() { if (call->isBlocked()) { diff --git a/src/sip/SIPCallManager.h b/src/sip/SIPCallManager.h index cd96106e..f333cbf7 100644 --- a/src/sip/SIPCallManager.h +++ b/src/sip/SIPCallManager.h @@ -116,6 +116,7 @@ class SIPCallManager : public QObject void isConferenceModeChanged(); void callContactChanged(SIPCall *call); void metadataChanged(SIPCall *call); + void callDelayChanged(SIPCall *call); void capabilitiesChanged(SIPCall *call); void audioLevelChanged(SIPCall *call, qreal level); void showCallWindow(); diff --git a/src/ui/CallsModel.cpp b/src/ui/CallsModel.cpp index ca39556c..a0d9febe 100644 --- a/src/ui/CallsModel.cpp +++ b/src/ui/CallsModel.cpp @@ -23,6 +23,7 @@ CallsModel::CallsModel(QObject *parent) : QAbstractListModel{ parent } if (index >= 0) { callInfo->isEstablished = call->isEstablished(); callInfo->established = call->establishedTime(); + callInfo->callDelay = call->callDelay(); callInfo->hasCapabilityJitsi = call->hasCapability("jitsi") && callInfo->isEstablished; auto idx = createIndex(index, 0); @@ -30,11 +31,23 @@ CallsModel::CallsModel(QObject *parent) : QAbstractListModel{ parent } { static_cast(Roles::IsEstablished), static_cast(Roles::EstablishedTime), + static_cast(Roles::CallDelay), static_cast(Roles::HasCapabilityJitsi), }); } }); + connect(&callManager, &SIPCallManager::callDelayChanged, this, [this](SIPCall *call) { + auto callInfo = m_callsHash.value(call->getId()); + const auto index = m_calls.indexOf(callInfo); + if (index >= 0) { + callInfo->callDelay = call->callDelay(); + + auto idx = createIndex(index, 0); + Q_EMIT dataChanged(idx, idx, { static_cast(Roles::CallDelay) }); + } + }); + connect(&callManager, &SIPCallManager::metadataChanged, this, [this](SIPCall *call) { auto callInfo = m_callsHash.value(call->getId()); const auto index = m_calls.indexOf(callInfo); @@ -136,6 +149,7 @@ QHash CallsModel::roleNames() const { static_cast(Roles::Company), "company" }, { static_cast(Roles::IsEstablished), "isEstablished" }, { static_cast(Roles::EstablishedTime), "establishedTime" }, + { static_cast(Roles::CallDelay), "callDelay" }, { static_cast(Roles::IsHolding), "isHolding" }, { static_cast(Roles::IsBlocked), "isBlocked" }, { static_cast(Roles::StatusCode), "statusCode" }, @@ -179,6 +193,7 @@ void CallsModel::updateCalls() callInfo->accountId = qobject_cast(call->parent())->id(); callInfo->remoteUri = call->sipUrl(); callInfo->established = call->establishedTime(); + callInfo->callDelay = call->callDelay(); callInfo->isEstablished = call->isEstablished(); callInfo->isIncoming = call->isIncoming(); callInfo->isBlocked = call->isBlocked(); @@ -255,6 +270,9 @@ QVariant CallsModel::data(const QModelIndex &index, int role) const case static_cast(Roles::EstablishedTime): return callInfo->established; + case static_cast(Roles::CallDelay): + return callInfo->callDelay; + case static_cast(Roles::IsIncoming): return callInfo->isIncoming; diff --git a/src/ui/CallsModel.h b/src/ui/CallsModel.h index d7f07942..1d73bcc6 100644 --- a/src/ui/CallsModel.h +++ b/src/ui/CallsModel.h @@ -30,6 +30,7 @@ class CallsModel : public QAbstractListModel qreal incomingAudioLevel = 0.0; bool hasMetadata = false; QDateTime established; + int callDelay = -1; ContactInfo contactInfo; pjsip_status_code statusCode = PJSIP_SC_NULL; }; @@ -46,6 +47,7 @@ class CallsModel : public QAbstractListModel Company, IsEstablished, EstablishedTime, + CallDelay, IsHolding, IsBlocked, StatusCode, diff --git a/src/ui/components/CallButtonBar.qml b/src/ui/components/CallButtonBar.qml index 6672c897..8cd81e43 100644 --- a/src/ui/components/CallButtonBar.qml +++ b/src/ui/components/CallButtonBar.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import QtCore import QtQuick import QtQuick.Controls.Material import base @@ -22,6 +23,7 @@ Item { readonly property bool isFinished: control.callItem?.isFinished ?? false readonly property bool isIncoming: control.callItem?.isIncoming ?? false readonly property bool hasCapabilityJitsi: control.callItem?.hasCapabilityJitsi ?? false + readonly property int callDelay: control.callItem?.callDelay ?? -1 readonly property bool areInCallButtonsEnabled: control.isEstablished && !control.isFinished @@ -60,14 +62,32 @@ Item { } } - Label { - id: elapsedTimeLabel - color: Theme.secondaryTextColor - text: "🕓 " + ViewHelper.secondsToNiceText(internal.elapsedSeconds) + Row { + spacing: 20 + leftPadding: 20 anchors { - verticalCenter: parent.verticalCenter + top: parent.top + bottom: parent.bottom left: parent.left - leftMargin: 20 + } + + Label { + id: elapsedTimeLabel + color: Theme.secondaryTextColor + text: "🕓 " + ViewHelper.secondsToNiceText(internal.elapsedSeconds) + anchors { + verticalCenter: parent.verticalCenter + } + } + + Label { + id: dtmfDebugLabel + visible: control.callDelay > 0 + color: Theme.secondaryTextColor + text: "⚠ " + control.callDelay + " ms" + anchors { + verticalCenter: parent.verticalCenter + } } } diff --git a/src/ui/components/CallItem.qml b/src/ui/components/CallItem.qml index 9bba24e2..dd41009e 100644 --- a/src/ui/components/CallItem.qml +++ b/src/ui/components/CallItem.qml @@ -24,6 +24,7 @@ Rectangle { required property bool isIncoming required property bool isEstablished required property date establishedTime + required property int callDelay required property bool isHolding required property bool isFinished required property bool hasCapabilityJitsi @@ -41,7 +42,6 @@ Rectangle { signal clicked - states: [ State { when: control.isHolding