From 9d6b2639a16a93bbd2ac6a078974af0f7f7a99c9 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Fri, 23 Jan 2026 17:14:17 +0100 Subject: [PATCH 01/12] feat: added pjsua2 dtmf receival --- src/media/AudioPort.cpp | 2 +- src/sip/SIPCall.cpp | 5 +++++ src/sip/SIPCall.h | 1 + src/sip/SIPCallManager.cpp | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index 17b97f9a..1a00e9e4 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -253,7 +253,7 @@ void AudioPort::onFrameReceived(pj::MediaFrame &frame) } if (m_io.isNull()) { - qCWarning(lcAudioPort) << "frame received, but no IO available"; + //qCWarning(lcAudioPort) << "frame received, but no IO available"; return; } diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index a9f662ac..3ee205ec 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -348,6 +348,11 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) qCWarning(lcSIPCall) << "failed to send message:" << prm.code << prm.reason; } +void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) +{ + qCWarning(lcSIPCall) << "GOT DTMF:" << prm.digit; +} + pj::AudioMedia *SIPCall::audioMedia() const { const auto callInfo = getInfo(); diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index 88c0b187..96ca72f0 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -28,6 +28,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; }; diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index 7a06aa43..3f8f19fb 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -896,6 +896,7 @@ void SIPCallManager::dispatchDtmfBuffer() m_dtmfGen = new DtmfGenerator(this); } + qCCritical(lcSIPCallManager) << "SENT DTMF:" << val.first(1).toStdString(); m_dtmfGen->playDtmf(val.front()); call->dialDtmf(val.first(1).toStdString()); From 16e3687d345f3053e82e83f1584da0bba7ceb233 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Mon, 26 Jan 2026 08:51:43 +0100 Subject: [PATCH 02/12] chore: add debug info basics --- src/sip/SIPCall.cpp | 24 ++++++++++++------------ src/sip/SIPCall.h | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 3ee205ec..e1958337 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -353,18 +353,6 @@ void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) qCWarning(lcSIPCall) << "GOT DTMF:" << prm.digit; } -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; -} - void SIPCall::onCallTsxState(pj::OnCallTsxStateParam &prm) { const auto header = QString::fromStdString(prm.e.body.tsxState.src.rdata.wholeMsg); @@ -382,6 +370,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); diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index 96ca72f0..bd2ef14d 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -51,6 +51,19 @@ class SIPCall : public ICallState, public pj::Call bool hasMetadata() const { return m_hasMetadata; } QList metadata() { return m_metadata; }; + // TODO: Move someplace else + // In SIPCall loop (e.g. its timer) run 10 sec metadata loop, send dtmf + timestamp and IM + // Wait for answer, add to debug() list + struct CallMetadata + { + QDateTime sent; + QDateTime received; + qint64 delay; + QChar digit; + }; + + QList debug() { return m_debug; } + bool hold(); bool unhold(); bool isHolding() const { return m_isHolding; } @@ -100,6 +113,8 @@ private Q_SLOTS: QList m_metadata; + QList m_debug; + pj::AudioMedia *m_aud_med = NULL; IMHandler *m_imHandler = nullptr; From 8d91a8ee7ec72dbd4e3978a901857709ef1a714d Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Tue, 27 Jan 2026 17:29:38 +0100 Subject: [PATCH 03/12] chore: sync dtmf progress --- src/sip/IMHandler.cpp | 27 +++++++++++++++++++++++++++ src/sip/SIPCall.cpp | 23 ++++++++++++++++++++++- src/sip/SIPCall.h | 28 ++++++++++++++++------------ 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 703eba2a..66a2b03f 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -88,6 +88,33 @@ bool IMHandler::process(const QString &contentType, const QString &message) return true; } + // Call delays + else if (path == "callDelay") { + bool validTimestamp = false; + qint64 timestamp; + 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->initializeCallDelay(timestamp, digit); + + return true; + } + return false; } diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index e1958337..0ae49d35 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -72,6 +72,9 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool globalCallState.holdAllCalls(this); } } + + m_callDelayCycleTimer.setInterval(10s); + connect(&m_callDelayCycleTimer, &QTimer::timeout, this, [this]() { /* TODO: Some IM + DTMF init sender */ }); } SIPCall::~SIPCall() @@ -350,7 +353,8 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) { - qCWarning(lcSIPCall) << "GOT DTMF:" << prm.digit; + // TODO: Don't just block all dtmf receivals for delay checks? + calculateCallDelay(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); } void SIPCall::onCallTsxState(pj::OnCallTsxStateParam &prm) @@ -651,3 +655,20 @@ void SIPCall::addMetadata(const QString &data) Q_EMIT metadataChanged(); } + +void SIPCall::initializeCallDelay(qint64 timestamp, QString digit) +{ + m_callDelay.sent = timestamp; + m_callDelay.digit = digit; +} + +void SIPCall::calculateCallDelay(qint64 timestamp, QString digit) +{ + m_callDelay.received = timestamp; + + if (digit != m_callDelay.digit) { + m_callDelay.latency = -1; + } else { + m_callDelay.latency = m_callDelay.received - m_callDelay.sent; + } +} diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index bd2ef14d..4de230cb 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include @@ -51,18 +52,11 @@ class SIPCall : public ICallState, public pj::Call bool hasMetadata() const { return m_hasMetadata; } QList metadata() { return m_metadata; }; - // TODO: Move someplace else - // In SIPCall loop (e.g. its timer) run 10 sec metadata loop, send dtmf + timestamp and IM - // Wait for answer, add to debug() list - struct CallMetadata - { - QDateTime sent; - QDateTime received; - qint64 delay; - QChar digit; - }; - QList debug() { return m_debug; } + /// Call party send IM with timestamp and dtmf digit it has sent + void initializeCallDelay(qint64 timestamp, QString digit); + /// Dtmf digit receival is logged with timestamp, followed by determining the delay + void calculateCallDelay(qint64 timestamp, QString digit); bool hold(); bool unhold(); @@ -113,7 +107,17 @@ private Q_SLOTS: QList m_metadata; - QList m_debug; + QTimer m_callDelayCycleTimer; + + struct CallDelay + { + qint64 sent; + qint64 received; + qint64 latency; + QString digit; + }; + + CallDelay m_callDelay; pj::AudioMedia *m_aud_med = NULL; IMHandler *m_imHandler = nullptr; From 818f8d1cf88be062fc28d07b0408674b1e029074 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Wed, 28 Jan 2026 13:03:41 +0100 Subject: [PATCH 04/12] chore: sync im/dtmf changes --- src/sip/IMHandler.cpp | 2 +- src/sip/SIPCall.cpp | 56 +++++++++++++++++++++++++++++++++++-------- src/sip/SIPCall.h | 20 ++++++---------- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 66a2b03f..5820a410 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -110,7 +110,7 @@ bool IMHandler::process(const QString &contentType, const QString &message) return false; } - m_call->initializeCallDelay(timestamp, digit); + m_call->setCallDelayTx(timestamp, digit); return true; } diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 0ae49d35..44b39088 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" @@ -74,7 +76,12 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool } m_callDelayCycleTimer.setInterval(10s); - connect(&m_callDelayCycleTimer, &QTimer::timeout, this, [this]() { /* TODO: Some IM + DTMF init sender */ }); + connect(&m_callDelayCycleTimer, &QTimer::timeout, this, [this]() { + QString digit = QString("%1").arg((m_callDelayCounter % 9) + 1); + m_callDelayCounter++; + + requestCallDelay(digit); + }); } SIPCall::~SIPCall() @@ -354,7 +361,7 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) { // TODO: Don't just block all dtmf receivals for delay checks? - calculateCallDelay(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); + setCallDelayRx(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); } void SIPCall::onCallTsxState(pj::OnCallTsxStateParam &prm) @@ -656,19 +663,48 @@ void SIPCall::addMetadata(const QString &data) Q_EMIT metadataChanged(); } -void SIPCall::initializeCallDelay(qint64 timestamp, QString digit) +void SIPCall::requestCallDelay(QString digit) { - m_callDelay.sent = timestamp; - m_callDelay.digit = 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::calculateCallDelay(qint64 timestamp, QString digit) +void SIPCall::setCallDelayRx(qint64 timestamp, QString digit) { - m_callDelay.received = timestamp; + m_callDelayRx.first = timestamp; - if (digit != m_callDelay.digit) { - m_callDelay.latency = -1; + if (digit != m_callDelayRx.second) { + m_callDelay = -1; } else { - m_callDelay.latency = m_callDelay.received - m_callDelay.sent; + m_callDelay = m_callDelayRx.first - m_callDelayTx.first; } + + qCWarning(lcSIPCall) << "call delay is" << m_callDelay << "ms"; } diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index 4de230cb..cdaa7927 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -52,11 +52,11 @@ class SIPCall : public ICallState, public pj::Call bool hasMetadata() const { return m_hasMetadata; } QList metadata() { return m_metadata; }; - + void requestCallDelay(QString digit); /// Call party send IM with timestamp and dtmf digit it has sent - void initializeCallDelay(qint64 timestamp, QString digit); + void setCallDelayTx(qint64 timestamp, QString digit); /// Dtmf digit receival is logged with timestamp, followed by determining the delay - void calculateCallDelay(qint64 timestamp, QString digit); + void setCallDelayRx(qint64 timestamp, QString digit); bool hold(); bool unhold(); @@ -108,16 +108,10 @@ private Q_SLOTS: QList m_metadata; QTimer m_callDelayCycleTimer; - - struct CallDelay - { - qint64 sent; - qint64 received; - qint64 latency; - QString digit; - }; - - CallDelay m_callDelay; + QPair m_callDelayTx; + QPair m_callDelayRx; + int m_callDelayCounter = 0; + qint64 m_callDelay; pj::AudioMedia *m_aud_med = NULL; IMHandler *m_imHandler = nullptr; From dac7435482d060ad86970e69d511df59edb0bf3c Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Wed, 28 Jan 2026 17:30:48 +0100 Subject: [PATCH 05/12] chore: working ping pong example --- src/sip/SIPCall.cpp | 7 ++++++- src/sip/SIPCallManager.cpp | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 44b39088..dd384ac9 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -227,6 +227,8 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) hold(); } }); + + m_callDelayCycleTimer.start(); } // Send DTMF post tasks if present @@ -275,6 +277,8 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) m_isEstablished = false; m_earlyCallState = false; + m_callDelayCycleTimer.stop(); + break; default: @@ -699,8 +703,9 @@ void SIPCall::setCallDelayTx(qint64 timestamp, QString digit) void SIPCall::setCallDelayRx(qint64 timestamp, QString digit) { m_callDelayRx.first = timestamp; + m_callDelayRx.second = digit; - if (digit != m_callDelayRx.second) { + if (m_callDelayRx.second != m_callDelayTx.second) { m_callDelay = -1; } else { m_callDelay = m_callDelayRx.first - m_callDelayTx.first; diff --git a/src/sip/SIPCallManager.cpp b/src/sip/SIPCallManager.cpp index 3f8f19fb..7a06aa43 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -896,7 +896,6 @@ void SIPCallManager::dispatchDtmfBuffer() m_dtmfGen = new DtmfGenerator(this); } - qCCritical(lcSIPCallManager) << "SENT DTMF:" << val.first(1).toStdString(); m_dtmfGen->playDtmf(val.front()); call->dialDtmf(val.first(1).toStdString()); From 249ad03df54dc90a710ccf083634c3594f762129 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Thu, 29 Jan 2026 12:28:49 +0100 Subject: [PATCH 06/12] feat: add dtmf debug config option --- src/sip/IMHandler.cpp | 6 +++--- src/sip/IMHandler.h | 6 ++++++ src/sip/SIPCall.cpp | 8 ++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 5820a410..3e18f5af 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_dtmfDebugEnabled = settings.value("generic/dtmfDebugEnabled", false).toBool(); } bool IMHandler::process(const QString &contentType, const QString &message) @@ -89,7 +89,7 @@ bool IMHandler::process(const QString &contentType, const QString &message) } // Call delays - else if (path == "callDelay") { + else if (path == "callDelay" && dtmfDebugEnabled()) { bool validTimestamp = false; qint64 timestamp; QString digit; diff --git a/src/sip/IMHandler.h b/src/sip/IMHandler.h index 5d3799e3..19278c54 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_dtmfDebugEnabled; + } + bool process(const QString &contentType, const QString &message); bool capabilitiesSent() const { return m_capabilitiesSent; } @@ -51,6 +56,7 @@ class IMHandler : public QObject unsigned m_capabilitySendingTries = 3; + bool m_dtmfDebugEnabled = false; bool m_capabilitiesSent = false; bool m_jitsiPreconfig = false; }; diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index dd384ac9..eb236755 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -228,7 +228,9 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) } }); - m_callDelayCycleTimer.start(); + if (m_imHandler->dtmfDebugEnabled()) { + m_callDelayCycleTimer.start(); + } } // Send DTMF post tasks if present @@ -277,7 +279,9 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) m_isEstablished = false; m_earlyCallState = false; - m_callDelayCycleTimer.stop(); + if (m_imHandler->dtmfDebugEnabled() && m_callDelayCycleTimer.isActive()) { + m_callDelayCycleTimer.stop(); + } break; From 7a6ae2b26da1f2885fa8667248ac3684284e0551 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Thu, 29 Jan 2026 12:56:42 +0100 Subject: [PATCH 07/12] feat: add dtmf debug call display info --- src/sip/SIPCall.h | 3 ++- src/ui/components/CallButtonBar.qml | 39 ++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index cdaa7927..aacdf836 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -71,6 +71,7 @@ class SIPCall : public ICallState, public pj::Call bool isEstablished() const { return m_isEstablished; } /// The time when the call was established (i.e. answered); invalid QDateTime if not established QDateTime establishedTime() const { return m_establishedTime; } + qint64 callDelay() const { return m_callDelay; } bool earlyCallState() const { return m_earlyCallState; } @@ -111,7 +112,7 @@ private Q_SLOTS: QPair m_callDelayTx; QPair m_callDelayRx; int m_callDelayCounter = 0; - qint64 m_callDelay; + qint64 m_callDelay = -1; pj::AudioMedia *m_aud_med = NULL; IMHandler *m_imHandler = nullptr; diff --git a/src/ui/components/CallButtonBar.qml b/src/ui/components/CallButtonBar.qml index 6672c897..ea9940c3 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 @@ -49,6 +50,14 @@ Item { } } + Settings { + id: settings + location: ViewHelper.userConfigPath + category: "generic" + + property bool dtmfDebugEnabled + } + Rectangle { id: topBorder height: 1 @@ -60,14 +69,32 @@ Item { } } - Label { - id: elapsedTimeLabel - color: Theme.secondaryTextColor - text: "🕓 " + ViewHelper.secondsToNiceText(internal.elapsedSeconds) + Row { + spacing: 5 + 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: settings.dtmfDebugEnabled + color: Theme.secondaryTextColor + text: "⚠ " + (control.callItem?.callDelay ?? -1) + " ms" + anchors { + verticalCenter: parent.verticalCenter + } } } From 929734efbccb776402a6fac67fb844d4bafecd71 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Thu, 29 Jan 2026 17:37:20 +0100 Subject: [PATCH 08/12] chore: ... --- src/sip/IMHandler.cpp | 5 +++-- src/sip/IMHandler.h | 3 +-- src/sip/SIPCall.cpp | 10 ++++++---- src/sip/SIPCall.h | 6 ++++-- src/sip/SIPCallManager.cpp | 2 ++ src/sip/SIPCallManager.h | 1 + src/ui/CallsModel.cpp | 18 ++++++++++++++++++ src/ui/CallsModel.h | 2 ++ src/ui/components/CallButtonBar.qml | 13 +++---------- src/ui/components/CallItem.qml | 2 +- 10 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 3e18f5af..3b54fc6f 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -24,8 +24,6 @@ IMHandler::IMHandler(SIPCall *parent) : QObject(parent), m_call(parent) settings.endGroup(); } - - m_dtmfDebugEnabled = settings.value("generic/dtmfDebugEnabled", false).toBool(); } bool IMHandler::process(const QString &contentType, const QString &message) @@ -180,6 +178,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 19278c54..3622af93 100644 --- a/src/sip/IMHandler.h +++ b/src/sip/IMHandler.h @@ -20,7 +20,7 @@ class IMHandler : public QObject } bool dtmfDebugEnabled() const { - return m_dtmfDebugEnabled; + return m_capabilities.contains("callDelay"); } bool process(const QString &contentType, const QString &message); @@ -56,7 +56,6 @@ class IMHandler : public QObject unsigned m_capabilitySendingTries = 3; - bool m_dtmfDebugEnabled = false; bool m_capabilitiesSent = false; bool m_jitsiPreconfig = false; }; diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index eb236755..1fcd54f0 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -228,9 +228,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) } }); - if (m_imHandler->dtmfDebugEnabled()) { - m_callDelayCycleTimer.start(); - } + m_callDelayCycleTimer.start(); } // Send DTMF post tasks if present @@ -279,7 +277,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) m_isEstablished = false; m_earlyCallState = false; - if (m_imHandler->dtmfDebugEnabled() && m_callDelayCycleTimer.isActive()) { + if (m_callDelayCycleTimer.isActive()) { m_callDelayCycleTimer.stop(); } @@ -368,6 +366,8 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) { + qCWarning(lcSIPCall) << "???"; + // TODO: Don't just block all dtmf receivals for delay checks? setCallDelayRx(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); } @@ -716,4 +716,6 @@ void SIPCall::setCallDelayRx(qint64 timestamp, QString digit) } qCWarning(lcSIPCall) << "call delay is" << m_callDelay << "ms"; + + Q_EMIT callDelayChanged(); } diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index aacdf836..834882a4 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -71,7 +71,8 @@ class SIPCall : public ICallState, public pj::Call bool isEstablished() const { return m_isEstablished; } /// The time when the call was established (i.e. answered); invalid QDateTime if not established QDateTime establishedTime() const { return m_establishedTime; } - qint64 callDelay() const { return m_callDelay; } + + int callDelay() const { return m_callDelay; } bool earlyCallState() const { return m_earlyCallState; } @@ -90,6 +91,7 @@ class SIPCall : public ICallState, public pj::Call void capabilitiesChanged(); void contactChanged(); void metadataChanged(); + void callDelayChanged(); private Q_SLOTS: void updateIsBlocked(); @@ -112,7 +114,7 @@ private Q_SLOTS: QPair m_callDelayTx; QPair m_callDelayRx; int m_callDelayCounter = 0; - qint64 m_callDelay = -1; + 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 7a06aa43..ddcf72c3 100644 --- a/src/sip/SIPCallManager.cpp +++ b/src/sip/SIPCallManager.cpp @@ -678,6 +678,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..c8eaec3b 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 = 0; 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 ea9940c3..eb462727 100644 --- a/src/ui/components/CallButtonBar.qml +++ b/src/ui/components/CallButtonBar.qml @@ -23,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 @@ -50,14 +51,6 @@ Item { } } - Settings { - id: settings - location: ViewHelper.userConfigPath - category: "generic" - - property bool dtmfDebugEnabled - } - Rectangle { id: topBorder height: 1 @@ -89,9 +82,9 @@ Item { Label { id: dtmfDebugLabel - visible: settings.dtmfDebugEnabled + visible: control.callDelay > 0 color: Theme.secondaryTextColor - text: "⚠ " + (control.callItem?.callDelay ?? -1) + " ms" + 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 From d76db8b48a0a965d91e294013ec135661d3e82d3 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Fri, 30 Jan 2026 13:36:17 +0100 Subject: [PATCH 09/12] feat: finalize dtmf debug loop --- src/sip/IMHandler.cpp | 2 ++ src/sip/IMHandler.h | 3 ++- src/sip/SIPCall.cpp | 16 ++++++++-------- src/ui/components/CallButtonBar.qml | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index 3b54fc6f..a6c1a0df 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -24,6 +24,8 @@ IMHandler::IMHandler(SIPCall *parent) : QObject(parent), m_call(parent) settings.endGroup(); } + + m_dtmfDebugConfigured = settings.value("generic/dtmfDebug", false).toBool(); } bool IMHandler::process(const QString &contentType, const QString &message) diff --git a/src/sip/IMHandler.h b/src/sip/IMHandler.h index 3622af93..3048428d 100644 --- a/src/sip/IMHandler.h +++ b/src/sip/IMHandler.h @@ -20,7 +20,7 @@ class IMHandler : public QObject } bool dtmfDebugEnabled() const { - return m_capabilities.contains("callDelay"); + return m_dtmfDebugConfigured && m_capabilities.contains("callDelay"); } bool process(const QString &contentType, const QString &message); @@ -58,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 1fcd54f0..9f37bef2 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -82,6 +82,12 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool requestCallDelay(digit); }); + + connect(this, &SIPCall::capabilitiesChanged, [this]() { + if (m_imHandler->dtmfDebugEnabled() && isEstablished() && !m_callDelayCycleTimer.isActive()) { + m_callDelayCycleTimer.start(); + } + }); } SIPCall::~SIPCall() @@ -227,8 +233,6 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) hold(); } }); - - m_callDelayCycleTimer.start(); } // Send DTMF post tasks if present @@ -277,7 +281,7 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) m_isEstablished = false; m_earlyCallState = false; - if (m_callDelayCycleTimer.isActive()) { + if (m_imHandler->dtmfDebugEnabled() && m_callDelayCycleTimer.isActive()) { m_callDelayCycleTimer.stop(); } @@ -366,9 +370,7 @@ void SIPCall::onInstantMessageStatus(pj::OnInstantMessageStatusParam &prm) void SIPCall::onDtmfDigit(pj::OnDtmfDigitParam &prm) { - qCWarning(lcSIPCall) << "???"; - - // TODO: Don't just block all dtmf receivals for delay checks? + // INFO: Currently only used for DTMF call latency debugging setCallDelayRx(QDateTime::currentMSecsSinceEpoch(), QString::fromStdString(prm.digit)); } @@ -715,7 +717,5 @@ void SIPCall::setCallDelayRx(qint64 timestamp, QString digit) m_callDelay = m_callDelayRx.first - m_callDelayTx.first; } - qCWarning(lcSIPCall) << "call delay is" << m_callDelay << "ms"; - Q_EMIT callDelayChanged(); } diff --git a/src/ui/components/CallButtonBar.qml b/src/ui/components/CallButtonBar.qml index eb462727..8cd81e43 100644 --- a/src/ui/components/CallButtonBar.qml +++ b/src/ui/components/CallButtonBar.qml @@ -63,7 +63,7 @@ Item { } Row { - spacing: 5 + spacing: 20 leftPadding: 20 anchors { top: parent.top From 61ab4f0f9a6a12db28c5feac012044ebd1bdc061 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Mon, 9 Feb 2026 15:04:28 +0100 Subject: [PATCH 10/12] chore: cleanup --- src/media/AudioPort.cpp | 2 +- src/sip/SIPCall.h | 3 +-- src/ui/CallsModel.h | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index 1a00e9e4..17b97f9a 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -253,7 +253,7 @@ void AudioPort::onFrameReceived(pj::MediaFrame &frame) } if (m_io.isNull()) { - //qCWarning(lcAudioPort) << "frame received, but no IO available"; + qCWarning(lcAudioPort) << "frame received, but no IO available"; return; } diff --git a/src/sip/SIPCall.h b/src/sip/SIPCall.h index 834882a4..29592517 100644 --- a/src/sip/SIPCall.h +++ b/src/sip/SIPCall.h @@ -53,9 +53,8 @@ class SIPCall : public ICallState, public pj::Call QList metadata() { return m_metadata; }; void requestCallDelay(QString digit); - /// Call party send IM with timestamp and dtmf digit it has sent + void setCallDelayTx(qint64 timestamp, QString digit); - /// Dtmf digit receival is logged with timestamp, followed by determining the delay void setCallDelayRx(qint64 timestamp, QString digit); bool hold(); diff --git a/src/ui/CallsModel.h b/src/ui/CallsModel.h index c8eaec3b..1d73bcc6 100644 --- a/src/ui/CallsModel.h +++ b/src/ui/CallsModel.h @@ -30,7 +30,7 @@ class CallsModel : public QAbstractListModel qreal incomingAudioLevel = 0.0; bool hasMetadata = false; QDateTime established; - int callDelay = 0; + int callDelay = -1; ContactInfo contactInfo; pjsip_status_code statusCode = PJSIP_SC_NULL; }; From 65d5c3449fe3f9d97b2ab2f342c3e9f0f8980d96 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Mon, 9 Feb 2026 15:17:39 +0100 Subject: [PATCH 11/12] chore: formatting --- src/sip/SIPCall.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 9f37bef2..80b6ca66 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -84,7 +84,8 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool }); connect(this, &SIPCall::capabilitiesChanged, [this]() { - if (m_imHandler->dtmfDebugEnabled() && isEstablished() && !m_callDelayCycleTimer.isActive()) { + if (m_imHandler->dtmfDebugEnabled() && isEstablished() + && !m_callDelayCycleTimer.isActive()) { m_callDelayCycleTimer.start(); } }); From 08087fedf2dfed1022d407fe9ee5015f2ba973b2 Mon Sep 17 00:00:00 2001 From: Leon Jennebach Date: Tue, 10 Feb 2026 13:36:31 +0100 Subject: [PATCH 12/12] chore: fix uninitialized timestamp var --- src/sip/IMHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sip/IMHandler.cpp b/src/sip/IMHandler.cpp index a6c1a0df..9190d99c 100644 --- a/src/sip/IMHandler.cpp +++ b/src/sip/IMHandler.cpp @@ -91,7 +91,7 @@ bool IMHandler::process(const QString &contentType, const QString &message) // Call delays else if (path == "callDelay" && dtmfDebugEnabled()) { bool validTimestamp = false; - qint64 timestamp; + qint64 timestamp = 0; QString digit; QUrlQuery q(callUrl); auto qitems = q.queryItems();