|
| 1 | +// SPDX-FileCopyrightText: 2025 Linus Jahn <[email protected]> |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: LGPL-2.1-or-later |
| 4 | + |
| 5 | +#include <QXmppCallManager.h> |
| 6 | +#include <QXmppClient.h> |
| 7 | +#include <QXmppRosterManager.h> |
| 8 | + |
| 9 | +#include <chrono> |
| 10 | +#include <gst/gst.h> |
| 11 | + |
| 12 | +#include <QCoreApplication> |
| 13 | +#include <QTimer> |
| 14 | +#ifdef Q_OS_UNIX |
| 15 | +#include <csignal> |
| 16 | +#endif |
| 17 | + |
| 18 | +using namespace std::chrono_literals; |
| 19 | + |
| 20 | +#ifdef Q_OS_UNIX |
| 21 | +void handleSignal(int signal) |
| 22 | +{ |
| 23 | + if (signal == SIGINT) { |
| 24 | + // print newline |
| 25 | + qDebug() << ""; |
| 26 | + if (QCoreApplication::instance()) { |
| 27 | + QCoreApplication::instance()->quit(); |
| 28 | + } |
| 29 | + } |
| 30 | +} |
| 31 | +#endif |
| 32 | + |
| 33 | +void setupCallStream(QXmppCall *call) |
| 34 | +{ |
| 35 | + auto *gstPipeline = call->pipeline(); |
| 36 | + auto *stream = call->audioStream(); |
| 37 | + |
| 38 | + qDebug() << "[Call] Setup call stream" << stream->media(); |
| 39 | + if (stream->media() == u"audio") { |
| 40 | + // output receiving audio |
| 41 | + stream->setReceivePadCallback([gstPipeline](GstPad *receivePad) { |
| 42 | + GstElement *output = gst_parse_bin_from_description("audioresample ! audioconvert ! autoaudiosink", true, nullptr); |
| 43 | + if (!gst_bin_add(GST_BIN(gstPipeline), output)) { |
| 44 | + qFatal("Failed to add input to pipeline"); |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + gst_pad_link(receivePad, gst_element_get_static_pad(output, "sink")); |
| 49 | + gst_element_sync_state_with_parent(output); |
| 50 | + |
| 51 | + qDebug() << "[Call] receive pad set"; |
| 52 | + }); |
| 53 | + |
| 54 | + // record and send microphone |
| 55 | + stream->setSendPadCallback([gstPipeline](GstPad *sendPad) { |
| 56 | + GstElement *output = gst_parse_bin_from_description("autoaudiosrc ! audioconvert ! audioresample ! queue max-size-time=1000000", true, nullptr); |
| 57 | + if (!gst_bin_add(GST_BIN(gstPipeline), output)) { |
| 58 | + qFatal("Failed to add input to pipeline"); |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + gst_pad_link(gst_element_get_static_pad(output, "src"), sendPad); |
| 63 | + gst_element_sync_state_with_parent(output); |
| 64 | + |
| 65 | + qDebug() << "[Call] send pad set"; |
| 66 | + }); |
| 67 | + } |
| 68 | +}; |
| 69 | + |
| 70 | +int main(int argc, char *argv[]) |
| 71 | +{ |
| 72 | + QCoreApplication app(argc, argv); |
| 73 | + |
| 74 | +#ifdef Q_OS_UNIX |
| 75 | + // set signal handler for SIGINT (CTRL+C) |
| 76 | + std::signal(SIGINT, handleSignal); |
| 77 | +#endif |
| 78 | + |
| 79 | + QXmppClient client; |
| 80 | + auto *rosterManager = client.findExtension<QXmppRosterManager>(); |
| 81 | + auto *callManager = client.addNewExtension<QXmppCallManager>(); |
| 82 | + client.logger()->setLoggingType(QXmppLogger::StdoutLogging); |
| 83 | + client.logger()->setMessageTypes(QXmppLogger::MessageType::AnyMessage); |
| 84 | + |
| 85 | + // client config |
| 86 | + QXmppConfiguration config; |
| 87 | + config.setJid(qEnvironmentVariable("QXMPP_JID")); |
| 88 | + config.setPassword(qEnvironmentVariable("QXMPP_PASSWORD")); |
| 89 | + config.setResourcePrefix("Call"); |
| 90 | + |
| 91 | + // call manager config |
| 92 | + callManager->setStunServer(QHostAddress(QStringLiteral("stun.nextcloud.com")), 443); |
| 93 | + // callManager->setTurnServer(); |
| 94 | + // callManager->setTurnUser(client.configuration().jid()); |
| 95 | + // callManager->setTurnUser(client.configuration().password()); |
| 96 | + |
| 97 | + client.connectToServer(config); |
| 98 | + |
| 99 | + // on connect |
| 100 | + QObject::connect(&client, &QXmppClient::connected, &app, [&app, &config, rosterManager, callManager] { |
| 101 | + // wait 1 second for presence of other clients to arrive |
| 102 | + QTimer::singleShot(1s, [&app, &config, rosterManager, callManager] { |
| 103 | + // other resources of our account |
| 104 | + auto otherResources = rosterManager->getResources(config.jidBare()); |
| 105 | + otherResources.removeOne(config.resource()); |
| 106 | + if (otherResources.isEmpty()) { |
| 107 | + qDebug() << "[Call] No other clients to call on this account."; |
| 108 | + return; |
| 109 | + } |
| 110 | + |
| 111 | + // call first JID |
| 112 | + auto *call = callManager->call(config.jidBare() + u'/' + otherResources.first()); |
| 113 | + Q_ASSERT(call != nullptr); |
| 114 | + |
| 115 | + QObject::connect(call, &QXmppCall::connected, &app, [call]() { |
| 116 | + qDebug() << "[Call] Call to" << call->jid() << "connected!"; |
| 117 | + setupCallStream(call); |
| 118 | + }); |
| 119 | + |
| 120 | + QObject::connect(call, &QXmppCall::ringing, [call]() { |
| 121 | + qDebug() << "[Call] Ringing" << call->jid() << "..."; |
| 122 | + }); |
| 123 | + QObject::connect(call, &QXmppCall::finished, [call]() { |
| 124 | + qDebug() << "[Call] Call with" << call->jid() << "ended."; |
| 125 | + }); |
| 126 | + }); |
| 127 | + }); |
| 128 | + |
| 129 | + // on call |
| 130 | + QObject::connect(callManager, &QXmppCallManager::callReceived, &app, [&app](QXmppCall *call) { |
| 131 | + qDebug() << "[Call] Received incoming call from" << call->jid() << "-" << "Accepting."; |
| 132 | + call->accept(); |
| 133 | + if (call->audioStream()) { |
| 134 | + setupCallStream(call); |
| 135 | + } |
| 136 | + |
| 137 | + QObject::connect(call, &QXmppCall::streamCreated, call, [call](QXmppCallStream *stream) { |
| 138 | + setupCallStream(call); |
| 139 | + }); |
| 140 | + }); |
| 141 | + |
| 142 | + // disconnect from server to avoid having multiple open dead sessions when testing |
| 143 | + QObject::connect(&app, &QCoreApplication::aboutToQuit, &app, [&client]() { |
| 144 | + qDebug() << "Closing connection..."; |
| 145 | + client.disconnectFromServer(); |
| 146 | + }); |
| 147 | + |
| 148 | + return app.exec(); |
| 149 | +} |
0 commit comments