aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelvin Keskin <melvo@olomono.de>2022-10-01 18:07:16 +0200
committerGitHub <noreply@github.com>2022-10-01 18:07:16 +0200
commit58dfd25a6477fda887e8040e174190eb7929381d (patch)
tree365ace4d90b893d0fe85ded465b2f601b4a3574d
parentd757ce3bf70058a7b4a0570b6b440de30b692b21 (diff)
downloadqxmpp-58dfd25a6477fda887e8040e174190eb7929381d.tar.gz
Implement XEP-0167: Jingle RTP Sessions Informational Messages (#460)
-rw-r--r--doc/doap.xml2
-rw-r--r--src/base/QXmppJingleIq.cpp185
-rw-r--r--src/base/QXmppJingleIq.h46
-rw-r--r--src/client/QXmppCallManager.cpp3
-rw-r--r--tests/qxmppjingleiq/tst_qxmppjingleiq.cpp162
5 files changed, 357 insertions, 41 deletions
diff --git a/doc/doap.xml b/doc/doap.xml
index 19b245ab..02a64a19 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -283,7 +283,7 @@ SPDX-License-Identifier: CC0-1.0
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.2</xmpp:version>
<xmpp:since>0.2</xmpp:since>
- <xmpp:note>'rtcp-mux' of https://xmpp.org/extensions/xep-0167.html#format since 1.5, 'encryption' of https://xmpp.org/extensions/xep-0167.html#srtp, https://xmpp.org/extensions/xep-0167.html#info-active, https://xmpp.org/extensions/xep-0167.html#info-hold, https://xmpp.org/extensions/xep-0167.html#info-mute</xmpp:note>
+ <xmpp:note>'rtcp-mux' of https://xmpp.org/extensions/xep-0167.html#format, informational messages for session states 'active', 'hold', 'unhold', 'mute', 'unmute' since 1.5; Missing 'encryption' of https://xmpp.org/extensions/xep-0167.html#srtp</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
diff --git a/src/base/QXmppJingleIq.cpp b/src/base/QXmppJingleIq.cpp
index 36336f48..32d9549e 100644
--- a/src/base/QXmppJingleIq.cpp
+++ b/src/base/QXmppJingleIq.cpp
@@ -188,6 +188,64 @@ QXmppJingleIqContentPrivate::QXmppJingleIqContentPrivate()
{
}
+///
+/// \enum QXmppJingleIq::Creator
+///
+/// Party that originially generated the content type
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \struct QXmppJingleIq::RtpSessionStateActive
+///
+/// Actively participating in the session after having been on mute or having put the other party on
+/// hold
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \struct QXmppJingleIq::RtpSessionStateHold
+///
+/// Temporarily not listening for media from the other party
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \struct QXmppJingleIq::RtpSessionStateUnhold
+///
+/// Ending hold state
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \struct QXmppJingleIq::RtpSessionStateMuting
+///
+/// State for muting or unmuting
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \struct QXmppJingleIq::RtpSessionStateRinging
+///
+/// State after the callee acknowledged the call but did not yet interacted with it
+///
+/// \since QXmpp 1.5
+///
+
+///
+/// \typedef QXmppJingleIq::RtpSessionState
+///
+/// Contains the state of an RTP session as specified by \xep{0167, Jingle RTP Sessions}
+/// Informational Messages.
+///
+/// \since QXmpp 1.5
+///
+
/// Constructs an empty content.
QXmppJingleIq::Content::Content()
: d(new QXmppJingleIqContentPrivate())
@@ -849,11 +907,12 @@ public:
QList<QXmppJingleIq::Content> contents;
QXmppJingleIq::Reason reason;
- bool ringing;
+
+ std::optional<QXmppJingleIq::RtpSessionState> rtpSessionState;
};
QXmppJingleIqPrivate::QXmppJingleIqPrivate()
- : action(QXmppJingleIq::ContentAccept), ringing(false)
+ : action(QXmppJingleIq::ContentAccept)
{
}
@@ -968,24 +1027,39 @@ void QXmppJingleIq::setResponder(const QString &responder)
d->responder = responder;
}
+///
/// Returns true if the call is ringing.
-
+///
+/// \deprecated This method is deprecated since QXmpp 1.5. Use \c QXmppJingleIq::rtpSessionState()
+/// instead.
+///
bool QXmppJingleIq::ringing() const
{
- return d->ringing;
+ if (d->rtpSessionState) {
+ return std::holds_alternative<RtpSessionStateRinging>(*d->rtpSessionState);
+ }
+
+ return false;
}
+///
/// Set to true if the call is ringing.
///
/// \param ringing
-
+///
+/// \deprecated This method is deprecated since QXmpp 1.5. Use
+/// \c QXmppJingleIq::setRtpSessionState() instead.
+///
void QXmppJingleIq::setRinging(bool ringing)
{
- d->ringing = ringing;
+ if (ringing) {
+ d->rtpSessionState = RtpSessionStateRinging();
+ } else {
+ d->rtpSessionState = std::nullopt;
+ }
}
/// Returns the session ID.
-
QString QXmppJingleIq::sid() const
{
return d->sid;
@@ -1024,6 +1098,36 @@ void QXmppJingleIq::setMujiGroupChatJid(const QString &mujiGroupChatJid)
d->mujiGroupChatJid = mujiGroupChatJid;
}
+///
+/// Returns the state of an RTP session as specified by \xep{0167, Jingle RTP Sessions}
+/// Informational Messages.
+///
+/// \return the session's state
+///
+/// \since QXmpp 1.5
+///
+std::optional<QXmppJingleIq::RtpSessionState> QXmppJingleIq::rtpSessionState() const
+{
+ return d->rtpSessionState;
+}
+
+///
+/// Sets the state of an RTP session as specified by \xep{0167, Jingle RTP Sessions} Informational
+/// Messages.
+///
+/// The appropriate action is set as well.
+/// Thus, it is not needed to set it manually.
+///
+/// \param rtpSessionState session's state
+///
+/// \since QXmpp 1.5
+///
+void QXmppJingleIq::setRtpSessionState(const std::optional<RtpSessionState> &rtpSessionState)
+{
+ d->rtpSessionState = rtpSessionState;
+ d->action = Action::SessionInfo;
+}
+
/// \cond
bool QXmppJingleIq::isJingleIq(const QDomElement &element)
{
@@ -1055,7 +1159,7 @@ void QXmppJingleIq::parseElementFromChild(const QDomElement &element)
d->contents.clear();
QDomElement contentElement = jingleElement.firstChildElement(QStringLiteral("content"));
while (!contentElement.isNull()) {
- QXmppJingleIq::Content content;
+ Content content;
content.parse(contentElement);
addContent(content);
contentElement = contentElement.nextSiblingElement(QStringLiteral("content"));
@@ -1063,9 +1167,36 @@ void QXmppJingleIq::parseElementFromChild(const QDomElement &element)
QDomElement reasonElement = jingleElement.firstChildElement(QStringLiteral("reason"));
d->reason.parse(reasonElement);
- // ringing
- QDomElement ringingElement = jingleElement.firstChildElement(QStringLiteral("ringing"));
- d->ringing = (ringingElement.namespaceURI() == ns_jingle_rtp_info);
+ for (auto childElement = jingleElement.firstChildElement();
+ !childElement.isNull();
+ childElement = childElement.nextSiblingElement()) {
+ if (childElement.namespaceURI() == ns_jingle_rtp_info) {
+ const auto elementTag = childElement.tagName();
+
+ if (elementTag == QStringLiteral("active")) {
+ d->rtpSessionState = RtpSessionStateActive();
+ } else if (elementTag == QStringLiteral("hold")) {
+ d->rtpSessionState = RtpSessionStateHold();
+ } else if (elementTag == QStringLiteral("unhold")) {
+ d->rtpSessionState = RtpSessionStateUnhold();
+ } else if (const auto isMute = elementTag == QStringLiteral("mute"); isMute || elementTag == QStringLiteral("unmute")) {
+ RtpSessionStateMuting muting;
+ muting.isMute = isMute;
+
+ if (const auto creator = childElement.attribute(QStringLiteral("creator")); creator == QStringLiteral("initiator")) {
+ muting.creator = Initiator;
+ } else if (creator == QStringLiteral("responder")) {
+ muting.creator = Responder;
+ }
+
+ muting.name = childElement.attribute(QStringLiteral("name"));
+
+ d->rtpSessionState = muting;
+ } else if (elementTag == QStringLiteral("ringing")) {
+ d->rtpSessionState = RtpSessionStateRinging();
+ }
+ }
+ }
}
void QXmppJingleIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
@@ -1091,10 +1222,36 @@ void QXmppJingleIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
d->reason.toXml(writer);
- // ringing
- if (d->ringing) {
- writer->writeStartElement(QStringLiteral("ringing"));
+ const auto writeStartElementWithNamespace = [=](const QString &tagName) {
+ writer->writeStartElement(tagName);
writer->writeDefaultNamespace(ns_jingle_rtp_info);
+ };
+
+ if (d->rtpSessionState) {
+ if (std::holds_alternative<RtpSessionStateActive>(*d->rtpSessionState)) {
+ writeStartElementWithNamespace(QStringLiteral("active"));
+ } else if (std::holds_alternative<RtpSessionStateHold>(*d->rtpSessionState)) {
+ writeStartElementWithNamespace(QStringLiteral("hold"));
+ } else if (std::holds_alternative<RtpSessionStateUnhold>(*d->rtpSessionState)) {
+ writeStartElementWithNamespace(QStringLiteral("unhold"));
+ } else if (auto rtpSessionStateMuting = std::get_if<RtpSessionStateMuting>(&(*d->rtpSessionState))) {
+ if (rtpSessionStateMuting->isMute) {
+ writeStartElementWithNamespace(QStringLiteral("mute"));
+ } else {
+ writeStartElementWithNamespace(QStringLiteral("unmute"));
+ }
+
+ if (rtpSessionStateMuting->creator == Initiator) {
+ helperToXmlAddAttribute(writer, QStringLiteral("creator"), QStringLiteral("initiator"));
+ } else if (rtpSessionStateMuting->creator == Responder) {
+ helperToXmlAddAttribute(writer, QStringLiteral("creator"), QStringLiteral("responder"));
+ }
+
+ helperToXmlAddAttribute(writer, QStringLiteral("name"), rtpSessionStateMuting->name);
+ } else {
+ writeStartElementWithNamespace(QStringLiteral("ringing"));
+ }
+
writer->writeEndElement();
}
diff --git a/src/base/QXmppJingleIq.h b/src/base/QXmppJingleIq.h
index 61b16461..02ecc9a5 100644
--- a/src/base/QXmppJingleIq.h
+++ b/src/base/QXmppJingleIq.h
@@ -8,6 +8,8 @@
#include "QXmppIq.h"
+#include <variant>
+
#include <QHostAddress>
class QXmppJingleCandidatePrivate;
@@ -237,6 +239,42 @@ public:
TransportReplace
};
+ enum Creator {
+ /// The initiator generated the content type.
+ Initiator,
+ /// The responder generated the content type.
+ Responder
+ };
+
+ struct RtpSessionStateActive
+ {
+ };
+
+ struct RtpSessionStateHold
+ {
+ };
+
+ struct RtpSessionStateUnhold
+ {
+ };
+
+ struct RtpSessionStateMuting
+ {
+ /// True when temporarily not sending media to the other party but continuing to accept
+ /// media from it, false for ending mute state
+ bool isMute = true;
+ /// Creator of the corresponding session
+ Creator creator;
+ /// Session to be muted (e.g., only audio or video)
+ QString name;
+ };
+
+ struct RtpSessionStateRinging
+ {
+ };
+
+ using RtpSessionState = std::variant<RtpSessionStateActive, RtpSessionStateHold, RtpSessionStateUnhold, RtpSessionStateMuting, RtpSessionStateRinging>;
+
/// \internal
///
/// The QXmppJingleIq::Content class represents the "content" element of a
@@ -386,9 +424,12 @@ public:
QString responder() const;
void setResponder(const QString &responder);
- // XEP-0167: Jingle RTP Sessions
+#if QXMPP_DEPRECATED_SINCE(1, 5)
+ QT_DEPRECATED_X("Use QXmpp::rtpSessionState() instead")
bool ringing() const;
+ QT_DEPRECATED_X("Use QXmpp::setRtpSessionState() instead")
void setRinging(bool ringing);
+#endif
QString sid() const;
void setSid(const QString &sid);
@@ -396,6 +437,9 @@ public:
QString mujiGroupChatJid() const;
void setMujiGroupChatJid(const QString &mujiGroupChatJid);
+ std::optional<RtpSessionState> rtpSessionState() const;
+ void setRtpSessionState(const std::optional<RtpSessionState> &rtpSessionState);
+
/// \cond
static bool isJingleIq(const QDomElement &element);
/// \endcond
diff --git a/src/client/QXmppCallManager.cpp b/src/client/QXmppCallManager.cpp
index 807e09ad..5a2065a9 100644
--- a/src/client/QXmppCallManager.cpp
+++ b/src/client/QXmppCallManager.cpp
@@ -281,9 +281,8 @@ void QXmppCallManager::_q_jingleIqReceived(const QXmppJingleIq &iq)
QXmppJingleIq ringing;
ringing.setTo(call->jid());
ringing.setType(QXmppIq::Set);
- ringing.setAction(QXmppJingleIq::SessionInfo);
ringing.setSid(call->sid());
- ringing.setRinging(true);
+ ringing.setRtpSessionState(QXmppJingleIq::RtpSessionStateRinging());
call->d->sendRequest(ringing);
// notify user
diff --git a/tests/qxmppjingleiq/tst_qxmppjingleiq.cpp b/tests/qxmppjingleiq/tst_qxmppjingleiq.cpp
index b245a21d..be9e3d4c 100644
--- a/tests/qxmppjingleiq/tst_qxmppjingleiq.cpp
+++ b/tests/qxmppjingleiq/tst_qxmppjingleiq.cpp
@@ -34,10 +34,11 @@ private slots:
void testContentRtpFeedbackNegotiation();
void testSession();
void testTerminate();
+ void testRtpSessionState_data();
+ void testRtpSessionState();
void testAudioPayloadType();
void testVideoPayloadType();
void testPayloadTypeRtpFeedbackNegotiation();
- void testRinging();
};
void tst_QXmppJingleIq::testIsSdpParameter_data()
@@ -701,6 +702,143 @@ void tst_QXmppJingleIq::testTerminate()
serializePacket(session, xml);
}
+void tst_QXmppJingleIq::testRtpSessionState_data()
+{
+ QTest::addColumn<QByteArray>("xml");
+ QTest::addColumn<QString>("state");
+
+ QTest::newRow("active")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<active xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("active");
+ QTest::newRow("hold")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<hold xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("hold");
+ QTest::newRow("unhold")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<unhold xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("unhold");
+ QTest::newRow("mute")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<mute xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\" creator=\"initiator\" name=\"voice\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("mute");
+ QTest::newRow("unmute")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<unmute xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\" creator=\"responder\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("unmute");
+ QTest::newRow("ringing")
+ << QByteArrayLiteral("<iq type=\"set\">"
+ "<jingle xmlns=\"urn:xmpp:jingle:1\" action=\"session-info\">"
+ "<ringing xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\"/>"
+ "</jingle>"
+ "</iq>")
+ << QStringLiteral("ringing");
+}
+
+void tst_QXmppJingleIq::testRtpSessionState()
+{
+ QFETCH(QByteArray, xml);
+ QFETCH(QString, state);
+
+ QXmppJingleIq iq1;
+ QVERIFY(!iq1.rtpSessionState());
+ parsePacket(iq1, xml);
+
+ const auto rtpSessionState1 = *iq1.rtpSessionState();
+ if (state == QStringLiteral("active")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateActive>(rtpSessionState1));
+ } else if (state == QStringLiteral("hold")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateHold>(rtpSessionState1));
+ } else if (state == QStringLiteral("unhold")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateUnhold>(rtpSessionState1));
+ } else if (const auto isMute = state == QStringLiteral("mute"); isMute || state == QStringLiteral("unmute")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateMuting>(rtpSessionState1));
+
+ const auto stateMuting = std::get<QXmppJingleIq::RtpSessionStateMuting>(rtpSessionState1);
+ QCOMPARE(stateMuting.isMute, isMute);
+
+ if (isMute) {
+ QCOMPARE(stateMuting.creator, QXmppJingleIq::Initiator);
+ QCOMPARE(stateMuting.name, QStringLiteral("voice"));
+ } else {
+ QCOMPARE(stateMuting.creator, QXmppJingleIq::Responder);
+ QVERIFY(stateMuting.name.isEmpty());
+ }
+ } else if (state == QStringLiteral("ringing")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateRinging>(rtpSessionState1));
+ }
+
+ serializePacket(iq1, xml);
+
+ QXmppJingleIq iq2;
+ iq2.setType(QXmppIq::Set);
+ iq2.setId({});
+
+ if (state == QStringLiteral("active")) {
+ iq2.setRtpSessionState(QXmppJingleIq::RtpSessionStateActive());
+ } else if (state == QStringLiteral("hold")) {
+ iq2.setRtpSessionState(QXmppJingleIq::RtpSessionStateHold());
+ } else if (state == QStringLiteral("unhold")) {
+ iq2.setRtpSessionState(QXmppJingleIq::RtpSessionStateUnhold());
+ } else if (const auto isMute = state == QStringLiteral("mute"); isMute || state == QStringLiteral("unmute")) {
+ QXmppJingleIq::RtpSessionStateMuting stateMuting;
+ stateMuting.isMute = isMute;
+
+ if (isMute) {
+ stateMuting.creator = QXmppJingleIq::Initiator;
+ stateMuting.name = QStringLiteral("voice");
+ } else {
+ stateMuting.creator = QXmppJingleIq::Responder;
+ }
+
+ iq2.setRtpSessionState(stateMuting);
+ } else if (state == QStringLiteral("ringing")) {
+ iq2.setRtpSessionState(QXmppJingleIq::RtpSessionStateRinging());
+ }
+
+ const auto rtpSessionState2 = *iq2.rtpSessionState();
+ if (state == QStringLiteral("active")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateActive>(rtpSessionState2));
+ } else if (state == QStringLiteral("hold")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateHold>(rtpSessionState2));
+ } else if (state == QStringLiteral("unhold")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateUnhold>(rtpSessionState2));
+ } else if (const auto isMute = state == QStringLiteral("mute"); isMute || state == QStringLiteral("unmute")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateMuting>(rtpSessionState2));
+
+ const auto stateMuting = std::get<QXmppJingleIq::RtpSessionStateMuting>(rtpSessionState2);
+ QCOMPARE(stateMuting.isMute, isMute);
+
+ if (isMute) {
+ QCOMPARE(stateMuting.creator, QXmppJingleIq::Initiator);
+ QCOMPARE(stateMuting.name, QStringLiteral("voice"));
+ } else {
+ QCOMPARE(stateMuting.creator, QXmppJingleIq::Responder);
+ QVERIFY(stateMuting.name.isEmpty());
+ }
+ } else if (state == QStringLiteral("ringing")) {
+ QVERIFY(std::holds_alternative<QXmppJingleIq::RtpSessionStateRinging>(rtpSessionState2));
+ }
+
+ serializePacket(iq2, xml);
+}
+
void tst_QXmppJingleIq::testAudioPayloadType()
{
const QByteArray xml(R"(<payload-type id="103" name="L16" channels="2" clockrate="16000"/>)");
@@ -790,27 +928,5 @@ void tst_QXmppJingleIq::testPayloadTypeRtpFeedbackNegotiation()
serializePacket(payload2, xml);
}
-void tst_QXmppJingleIq::testRinging()
-{
- const QByteArray xml(
- "<iq"
- " id=\"tgr515bt\""
- " to=\"romeo@montague.lit/orchard\""
- " from=\"juliet@capulet.lit/balcony\""
- " type=\"set\">"
- "<jingle xmlns=\"urn:xmpp:jingle:1\""
- " action=\"session-info\""
- " initiator=\"romeo@montague.lit/orchard\""
- " sid=\"a73sjjvkla37jfea\">"
- "<ringing xmlns=\"urn:xmpp:jingle:apps:rtp:info:1\"/>"
- "</jingle>"
- "</iq>");
-
- QXmppJingleIq iq;
- parsePacket(iq, xml);
- QCOMPARE(iq.ringing(), true);
- serializePacket(iq, xml);
-}
-
QTEST_MAIN(tst_QXmppJingleIq)
#include "tst_qxmppjingleiq.moc"