diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8f83ce27f..29225cf99 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -22,4 +22,6 @@ add_simple_example(8_server) add_subdirectory(example_5_rpcInterface) add_subdirectory(example_6_rpcClient) add_subdirectory(example_9_vCard) - +if(WITH_GSTREAMER) + add_subdirectory(audio_call) +endif() diff --git a/examples/audio_call/AudioCall.cpp b/examples/audio_call/AudioCall.cpp new file mode 100644 index 000000000..4f9c3e16a --- /dev/null +++ b/examples/audio_call/AudioCall.cpp @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2025 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include + +#include +#include + +#include +#include +#ifdef Q_OS_UNIX +#include +#endif + +using namespace std::chrono_literals; + +#ifdef Q_OS_UNIX +void handleSignal(int signal) +{ + if (signal == SIGINT) { + // print newline + qDebug() << ""; + if (QCoreApplication::instance()) { + QCoreApplication::instance()->quit(); + } + } +} +#endif + +void setupCallStream(QXmppCall *call) +{ + auto *gstPipeline = call->pipeline(); + auto *stream = call->audioStream(); + + qDebug() << "[Call] Setup call stream" << stream->media(); + if (stream->media() == u"audio") { + // output receiving audio + stream->setReceivePadCallback([gstPipeline](GstPad *receivePad) { + GstElement *output = gst_parse_bin_from_description("audioresample ! audioconvert ! autoaudiosink", true, nullptr); + if (!gst_bin_add(GST_BIN(gstPipeline), output)) { + qFatal("Failed to add input to pipeline"); + return; + } + + gst_pad_link(receivePad, gst_element_get_static_pad(output, "sink")); + gst_element_sync_state_with_parent(output); + + qDebug() << "[Call] receive pad set"; + }); + + // record and send microphone + stream->setSendPadCallback([gstPipeline](GstPad *sendPad) { + GstElement *output = gst_parse_bin_from_description("autoaudiosrc ! audioconvert ! audioresample ! queue max-size-time=1000000", true, nullptr); + if (!gst_bin_add(GST_BIN(gstPipeline), output)) { + qFatal("Failed to add input to pipeline"); + return; + } + + gst_pad_link(gst_element_get_static_pad(output, "src"), sendPad); + gst_element_sync_state_with_parent(output); + + qDebug() << "[Call] send pad set"; + }); + } +}; + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + +#ifdef Q_OS_UNIX + // set signal handler for SIGINT (CTRL+C) + std::signal(SIGINT, handleSignal); +#endif + + QXmppClient client; + auto *rosterManager = client.findExtension(); + auto *callManager = client.addNewExtension(); + client.logger()->setLoggingType(QXmppLogger::StdoutLogging); + client.logger()->setMessageTypes(QXmppLogger::MessageType::AnyMessage); + + // client config + QXmppConfiguration config; + config.setJid(qEnvironmentVariable("QXMPP_JID")); + config.setPassword(qEnvironmentVariable("QXMPP_PASSWORD")); + config.setResourcePrefix("Call"); + + // call manager config + callManager->setStunServer(QHostAddress(QStringLiteral("stun.nextcloud.com")), 443); + // callManager->setTurnServer(); + // callManager->setTurnUser(client.configuration().jid()); + // callManager->setTurnUser(client.configuration().password()); + + client.connectToServer(config); + + auto setupCall = [&app, callManager](QXmppCall *call) { + if (call->audioStream()) { + setupCallStream(call); + } + + QObject::connect(call, &QXmppCall::streamCreated, call, [call](QXmppCallStream *stream) { + setupCallStream(call); + }); + + QObject::connect(call, &QXmppCall::connected, &app, [=]() { + qDebug() << "[Call] Call to" << call->jid() << "connected!"; + }); + QObject::connect(call, &QXmppCall::ringing, [=]() { + qDebug() << "[Call] Ringing" << call->jid() << "..."; + }); + QObject::connect(call, &QXmppCall::finished, [=]() { + qDebug() << "[Call] Call with" << call->jid() << "ended. (Deleting)"; + call->deleteLater(); + }); + }; + + // on connect + QObject::connect(&client, &QXmppClient::connected, &app, [=, &config] { + // wait 1 second for presence of other clients to arrive + QTimer::singleShot(1s, [=, &config] { + // other resources of our account + auto otherResources = rosterManager->getResources(config.jidBare()); + otherResources.removeOne(config.resource()); + if (otherResources.isEmpty()) { + qDebug() << "[Call] No other clients to call on this account."; + return; + } + + // call first JID + auto *call = callManager->call(config.jidBare() + u'/' + otherResources.first()); + Q_ASSERT(call != nullptr); + + setupCall(call); + }); + }); + + // on incoming call + QObject::connect(callManager, &QXmppCallManager::callReceived, &app, [=](QXmppCall *call) { + qDebug() << "[Call] Received incoming call from" << call->jid() << "-" << "Accepting."; + call->accept(); + + setupCall(call); + }); + + // disconnect from server to avoid having multiple open dead sessions when testing + QObject::connect(&app, &QCoreApplication::aboutToQuit, &app, [&client]() { + qDebug() << "Closing connection..."; + client.disconnectFromServer(); + }); + + return app.exec(); +} diff --git a/examples/audio_call/CMakeLists.txt b/examples/audio_call/CMakeLists.txt new file mode 100644 index 000000000..95a5372cd --- /dev/null +++ b/examples/audio_call/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Linus Jahn +# +# SPDX-License-Identifier: CC0-1.0 + +find_package(GStreamer REQUIRED) +find_package(GLIB2 REQUIRED) +find_package(GObject REQUIRED) + +add_executable(audio_call AudioCall.cpp) + +target_link_libraries(audio_call + PUBLIC + ${QXMPP_TARGET} + ${GLIB2_LIBRARIES} + ${GOBJECT_LIBRARIES} + ${GSTREAMER_LIBRARY} +) + +target_include_directories(audio_call + PUBLIC + ${QXMPP_TARGET} + ${GLIB2_INCLUDE_DIR} + ${GOBJECT_INCLUDE_DIR} + ${GSTREAMER_INCLUDE_DIRS} +) diff --git a/src/client/QXmppCall.cpp b/src/client/QXmppCall.cpp index 52b649526..7aadc7497 100644 --- a/src/client/QXmppCall.cpp +++ b/src/client/QXmppCall.cpp @@ -13,15 +13,22 @@ #include "QXmppConstants_p.h" #include "QXmppJingleIq.h" #include "QXmppStun.h" +#include "QXmppTask.h" #include "QXmppUtils.h" #include "StringLiterals.h" +#include +#include + +// gstreamer #include #include #include +using namespace std::chrono_literals; + /// \cond QXmppCallPrivate::QXmppCallPrivate(QXmppCall *qq) : direction(QXmppCall::IncomingDirection), @@ -95,21 +102,22 @@ void QXmppCallPrivate::padAdded(GstPad *pad) if (nameParts.size() < 4) { return; } - if (nameParts[0] == QLatin1String("send") && - nameParts[1] == QLatin1String("rtp") && - nameParts[2] == QLatin1String("src")) { + if (nameParts[0] == u"send" && + nameParts[1] == u"rtp" && + nameParts[2] == u"src") { if (nameParts.size() != 4) { return; } int sessionId = nameParts[3].toInt(); auto stream = findStreamById(sessionId); stream->d->addRtpSender(pad); - } else if (nameParts[0] == QLatin1String("recv") || - nameParts[1] == QLatin1String("rtp") || - nameParts[2] == QLatin1String("src")) { + } else if (nameParts[0] == u"recv" || + nameParts[1] == u"rtp" || + nameParts[2] == u"src") { if (nameParts.size() != 6) { return; } + int sessionId = nameParts[3].toInt(); int pt = nameParts[5].toInt(); auto stream = findStreamById(sessionId); @@ -147,7 +155,7 @@ GstCaps *QXmppCallPrivate::ptMap(uint sessionId, uint pt) return nullptr; } -bool QXmppCallPrivate::isFormatSupported(const QString &codecName) const +bool QXmppCallPrivate::isFormatSupported(const QString &codecName) { GstElementFactory *factory; factory = gst_element_factory_find(codecName.toLatin1().data()); @@ -158,68 +166,45 @@ bool QXmppCallPrivate::isFormatSupported(const QString &codecName) const return true; } -void QXmppCallPrivate::filterGStreamerFormats(QList &formats) +bool QXmppCallPrivate::isCodecSupported(const GstCodec &codec) { - auto it = formats.begin(); - while (it != formats.end()) { - bool supported = isFormatSupported(it->gstPay) && - isFormatSupported(it->gstDepay) && - isFormatSupported(it->gstEnc) && - isFormatSupported(it->gstDec); - if (!supported) { - it = formats.erase(it); - } else { - ++it; - } - } + return isFormatSupported(codec.gstPay) && + isFormatSupported(codec.gstDepay) && + isFormatSupported(codec.gstEnc) && + isFormatSupported(codec.gstDec); } -QXmppCallStream *QXmppCallPrivate::findStreamByMedia(const QString &media) +void QXmppCallPrivate::filterGStreamerFormats(QList &formats) { - for (auto stream : std::as_const(streams)) { - if (stream->media() == media) { - return stream; - } - } - return nullptr; + auto removedRange = std::ranges::remove_if(formats, std::not_fn(isCodecSupported)); + formats.erase(removedRange.begin(), removedRange.end()); } -QXmppCallStream *QXmppCallPrivate::findStreamByName(const QString &name) +QXmppCallStream *QXmppCallPrivate::findStreamByMedia(QStringView media) { - for (auto stream : std::as_const(streams)) { - if (stream->name() == name) { - return stream; - } + if (auto stream = std::ranges::find(streams, media, &QXmppCallStream::media); + stream != streams.end()) { + return *stream; } return nullptr; } -QXmppCallStream *QXmppCallPrivate::findStreamById(const int id) +QXmppCallStream *QXmppCallPrivate::findStreamByName(QStringView name) { - for (auto stream : std::as_const(streams)) { - if (stream->id() == id) { - return stream; - } + if (auto stream = std::ranges::find(streams, name, &QXmppCallStream::name); + stream != streams.end()) { + return *stream; } return nullptr; } -void QXmppCallPrivate::handleAck(const QXmppIq &ack) +QXmppCallStream *QXmppCallPrivate::findStreamById(int id) { - const QString id = ack.id(); - for (int i = 0; i < requests.size(); ++i) { - if (id == requests[i].id()) { - // process acknowledgement - const QXmppJingleIq request = requests.takeAt(i); - q->debug(u"Received ACK for packet %1"_s.arg(id)); - - // handle termination - if (request.action() == QXmppJingleIq::SessionTerminate) { - q->terminated(); - } - return; - } + if (auto stream = std::ranges::find(streams, id, &QXmppCallStream::id); + stream != streams.end()) { + return *stream; } + return nullptr; } bool QXmppCallPrivate::handleDescription(QXmppCallStream *stream, const QXmppJingleIq::Content &content) @@ -321,7 +306,9 @@ void QXmppCallPrivate::handleRequest(const QXmppJingleIq &iq) } else if (iq.action() == QXmppJingleIq::SessionInfo) { // notify user - QTimer::singleShot(0, q, SIGNAL(ringing())); + QTimer::singleShot(0, q, [this]() { + Q_EMIT q->ringing(); + }); } else if (iq.action() == QXmppJingleIq::SessionTerminate) { @@ -375,7 +362,7 @@ void QXmppCallPrivate::handleRequest(const QXmppJingleIq &iq) iq.setAction(QXmppJingleIq::ContentReject); iq.setSid(q->sid()); iq.reason().setType(QXmppJingleIq::Reason::FailedApplication); - sendRequest(iq); + manager->client()->sendIq(std::move(iq)); streams.removeAll(stream); delete stream; return; @@ -388,7 +375,7 @@ void QXmppCallPrivate::handleRequest(const QXmppJingleIq &iq) iq.setAction(QXmppJingleIq::ContentAccept); iq.setSid(q->sid()); iq.addContent(localContent(stream)); - sendRequest(iq); + manager->client()->sendIq(std::move(iq)); } else if (iq.action() == QXmppJingleIq::TransportInfo) { @@ -442,7 +429,7 @@ QXmppCallStream *QXmppCallPrivate::createStream(const QString &media, const QStr // connect signals QObject::connect(stream->d->connection, &QXmppIceConnection::localCandidatesChanged, - q, &QXmppCall::localCandidatesChanged); + q, [this, stream]() { q->onLocalCandidatesChanged(stream); }); QObject::connect(stream->d->connection, &QXmppIceConnection::disconnected, q, &QXmppCall::hangup); @@ -484,7 +471,7 @@ bool QXmppCallPrivate::sendAck(const QXmppJingleIq &iq) return manager->client()->sendPacket(ack); } -bool QXmppCallPrivate::sendInvite() +void QXmppCallPrivate::sendInvite() { // create audio stream QXmppCallStream *stream = findStreamByMedia(AUDIO_MEDIA); @@ -497,16 +484,7 @@ bool QXmppCallPrivate::sendInvite() iq.setInitiator(ownJid); iq.setSid(sid); iq.addContent(localContent(stream)); - return sendRequest(iq); -} - -/// -/// Sends a Jingle IQ and adds it to outstanding requests. -/// -bool QXmppCallPrivate::sendRequest(const QXmppJingleIq &iq) -{ - requests << iq; - return manager->client()->sendPacket(iq); + manager->client()->send(std::move(iq)); } void QXmppCallPrivate::setState(QXmppCall::State newState) @@ -540,11 +518,16 @@ void QXmppCallPrivate::terminate(QXmppJingleIq::Reason::Type reasonType) iq.setAction(QXmppJingleIq::SessionTerminate); iq.setSid(sid); iq.reason().setType(reasonType); - sendRequest(iq); + setState(QXmppCall::DisconnectingState); + manager->client()->sendIq(std::move(iq)).then(q, [this](auto result) { + // terminate on both success or error + q->terminated(); + }); + // schedule forceful termination in 5s - QTimer::singleShot(5000, q, &QXmppCall::terminated); + QTimer::singleShot(5s, q, &QXmppCall::terminated); } /// \endcond @@ -585,7 +568,7 @@ void QXmppCall::accept() iq.setResponder(d->ownJid); iq.setSid(d->sid); iq.addContent(d->localContent(stream)); - d->sendRequest(iq); + d->manager->client()->sendIq(std::move(iq)); // notify user Q_EMIT d->manager->callStarted(this); @@ -655,28 +638,15 @@ void QXmppCall::hangup() /// /// Sends a transport-info to inform the remote party of new local candidates. /// -void QXmppCall::localCandidatesChanged() +void QXmppCall::onLocalCandidatesChanged(QXmppCallStream *stream) { - // find the stream - QXmppIceConnection *conn = qobject_cast(sender()); - QXmppCallStream *stream = nullptr; - for (auto ptr : std::as_const(d->streams)) { - if (ptr->d->connection == conn) { - stream = ptr; - break; - } - } - if (!stream) { - return; - } - QXmppJingleIq iq; iq.setTo(d->jid); iq.setType(QXmppIq::Set); iq.setAction(QXmppJingleIq::TransportInfo); iq.setSid(d->sid); iq.addContent(d->localContent(stream)); - d->sendRequest(iq); + d->manager->client()->sendIq(std::move(iq)); } /// @@ -721,8 +691,8 @@ void QXmppCall::addVideo() } // create video stream - QLatin1String creator = (d->direction == QXmppCall::OutgoingDirection) ? QLatin1String("initiator") : QLatin1String("responder"); - stream = d->createStream(VIDEO_MEDIA, creator, QLatin1String("webcam")); + QString creator = (d->direction == QXmppCall::OutgoingDirection) ? u"initiator"_s : u"responder"_s; + stream = d->createStream(VIDEO_MEDIA.toString(), creator, u"webcam"_s); d->streams << stream; // build request @@ -732,5 +702,5 @@ void QXmppCall::addVideo() iq.setAction(QXmppJingleIq::ContentAdd); iq.setSid(d->sid); iq.addContent(d->localContent(stream)); - d->sendRequest(iq); + d->manager->client()->sendIq(std::move(iq)); } diff --git a/src/client/QXmppCall.h b/src/client/QXmppCall.h index 6650c4a00..ba4f1e9cf 100644 --- a/src/client/QXmppCall.h +++ b/src/client/QXmppCall.h @@ -82,8 +82,8 @@ class QXMPP_EXPORT QXmppCall : public QXmppLoggable Q_SLOT void addVideo(); private: - Q_SLOT void localCandidatesChanged(); - Q_SLOT void terminated(); + void onLocalCandidatesChanged(QXmppCallStream *stream); + void terminated(); QXmppCall(const QString &jid, QXmppCall::Direction direction, QXmppCallManager *parent); diff --git a/src/client/QXmppCallManager.cpp b/src/client/QXmppCallManager.cpp index 5da0218f3..eddd254f2 100644 --- a/src/client/QXmppCallManager.cpp +++ b/src/client/QXmppCallManager.cpp @@ -10,6 +10,7 @@ #include "QXmppClient.h" #include "QXmppConstants_p.h" #include "QXmppJingleIq.h" +#include "QXmppTask.h" #include "QXmppUtils.h" #include "StringLiterals.h" @@ -95,9 +96,6 @@ void QXmppCallManager::onRegistered(QXmppClient *client) connect(client, &QXmppClient::disconnected, this, &QXmppCallManager::_q_disconnected); - connect(client, &QXmppClient::iqReceived, - this, &QXmppCallManager::_q_iqReceived); - connect(client, &QXmppClient::presenceReceived, this, &QXmppCallManager::_q_presenceReceived); } @@ -107,9 +105,6 @@ void QXmppCallManager::onUnregistered(QXmppClient *client) disconnect(client, &QXmppClient::disconnected, this, &QXmppCallManager::_q_disconnected); - disconnect(client, &QXmppClient::iqReceived, - this, &QXmppCallManager::_q_iqReceived); - disconnect(client, &QXmppClient::presenceReceived, this, &QXmppCallManager::_q_presenceReceived); } @@ -228,21 +223,6 @@ void QXmppCallManager::_q_disconnected() } } -/// -/// Handles acknowledgements. -/// -void QXmppCallManager::_q_iqReceived(const QXmppIq &ack) -{ - if (ack.type() != QXmppIq::Result) { - return; - } - - // find request - for (auto *call : std::as_const(d->calls)) { - call->d->handleAck(ack); - } -} - /// /// Handles a Jingle IQ. /// @@ -292,7 +272,7 @@ void QXmppCallManager::_q_jingleIqReceived(const QXmppJingleIq &iq) ringing.setType(QXmppIq::Set); ringing.setSid(call->sid()); ringing.setRtpSessionState(QXmppJingleIq::RtpSessionStateRinging()); - call->d->sendRequest(ringing); + client()->sendIq(std::move(ringing)); // notify user Q_EMIT callReceived(call); diff --git a/src/client/QXmppCallManager.h b/src/client/QXmppCallManager.h index 015c5bcff..f7f5b1042 100644 --- a/src/client/QXmppCallManager.h +++ b/src/client/QXmppCallManager.h @@ -82,7 +82,6 @@ public Q_SLOTS: private Q_SLOTS: void _q_callDestroyed(QObject *object); void _q_disconnected(); - void _q_iqReceived(const QXmppIq &iq); void _q_jingleIqReceived(const QXmppJingleIq &iq); void _q_presenceReceived(const QXmppPresence &presence); diff --git a/src/client/QXmppCallStream.cpp b/src/client/QXmppCallStream.cpp index 0a7881cd4..596027080 100644 --- a/src/client/QXmppCallStream.cpp +++ b/src/client/QXmppCallStream.cpp @@ -97,7 +97,7 @@ QXmppCallStreamPrivate::QXmppCallStreamPrivate(QXmppCallStream *parent, GstEleme // We need frequent RTCP reports for the bandwidth controller GstElement *rtpSession; g_signal_emit_by_name(rtpbin, "get-session", static_cast(id), &rtpSession); - g_object_set(rtpSession, "rtcp-min-interval", 100000000, nullptr); + g_object_set(rtpSession, "rtcp-min-interval", 100'000'000, nullptr); gst_element_sync_state_with_parent(iceReceiveBin); gst_element_sync_state_with_parent(iceSendBin); diff --git a/src/client/QXmppCallStream_p.h b/src/client/QXmppCallStream_p.h index f898f0f60..59dd61f91 100644 --- a/src/client/QXmppCallStream_p.h +++ b/src/client/QXmppCallStream_p.h @@ -29,8 +29,8 @@ class QXmppIceConnection; static const int RTP_COMPONENT = 1; static const int RTCP_COMPONENT = 2; -static const QLatin1String AUDIO_MEDIA("audio"); -static const QLatin1String VIDEO_MEDIA("video"); +constexpr QStringView AUDIO_MEDIA = u"audio"; +constexpr QStringView VIDEO_MEDIA = u"video"; class QXmppCallStreamPrivate : public QObject { diff --git a/src/client/QXmppCall_p.h b/src/client/QXmppCall_p.h index 8b2f3e5c0..33a48dff2 100644 --- a/src/client/QXmppCall_p.h +++ b/src/client/QXmppCall_p.h @@ -52,30 +52,28 @@ class QXmppCallPrivate : public QObject void ssrcActive(uint sessionId, uint ssrc); void padAdded(GstPad *pad); GstCaps *ptMap(uint sessionId, uint pt); - bool isFormatSupported(const QString &codecName) const; - void filterGStreamerFormats(QList &formats); + static bool isFormatSupported(const QString &codecName); + static bool isCodecSupported(const GstCodec &codec); + static void filterGStreamerFormats(QList &formats); QXmppCallStream *createStream(const QString &media, const QString &creator, const QString &name); - QXmppCallStream *findStreamByMedia(const QString &media); - QXmppCallStream *findStreamByName(const QString &name); - QXmppCallStream *findStreamById(const int id); + QXmppCallStream *findStreamByMedia(QStringView media); + QXmppCallStream *findStreamByName(QStringView name); + QXmppCallStream *findStreamById(int id); QXmppJingleIq::Content localContent(QXmppCallStream *stream) const; - void handleAck(const QXmppIq &iq); bool handleDescription(QXmppCallStream *stream, const QXmppJingleIq::Content &content); void handleRequest(const QXmppJingleIq &iq); bool handleTransport(QXmppCallStream *stream, const QXmppJingleIq::Content &content); void setState(QXmppCall::State state); bool sendAck(const QXmppJingleIq &iq); - bool sendInvite(); - bool sendRequest(const QXmppJingleIq &iq); + void sendInvite(); void terminate(QXmppJingleIq::Reason::Type reasonType); QXmppCall::Direction direction; QString jid; QString ownJid; QXmppCallManager *manager; - QList requests; QString sid; QXmppCall::State state;