summaryrefslogtreecommitdiff
path: root/plugins/Jingle
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2023-07-09 15:03:09 +0300
committerGeorge Hazan <george.hazan@gmail.com>2023-07-09 15:03:09 +0300
commit7c33e4a07ba1f22a58c291018ca99a4f6becb1fe (patch)
tree4a8ef2882a0cc26e2f583b56fab9054d9f5e72f6 /plugins/Jingle
parent14e09a5f84023ad21a2f494ced01a991966abee4 (diff)
fixes #3351 (Jingle: Separate all VOIP-related code from Jabber to Jingle plugin)
Diffstat (limited to 'plugins/Jingle')
-rw-r--r--plugins/Jingle/Jingle.vcxproj2
-rw-r--r--plugins/Jingle/Jingle.vcxproj.filters6
-rw-r--r--plugins/Jingle/src/account.cpp242
-rw-r--r--plugins/Jingle/src/account.h26
-rw-r--r--plugins/Jingle/src/main.cpp13
-rw-r--r--plugins/Jingle/src/stdafx.h2
-rw-r--r--plugins/Jingle/src/voip.cpp525
7 files changed, 799 insertions, 17 deletions
diff --git a/plugins/Jingle/Jingle.vcxproj b/plugins/Jingle/Jingle.vcxproj
index 0f061b8cfe..4e625da708 100644
--- a/plugins/Jingle/Jingle.vcxproj
+++ b/plugins/Jingle/Jingle.vcxproj
@@ -31,11 +31,13 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
+ <ClCompile Include="..\..\protocols\JabberG\src\jabber_xml.cpp" />
<ClCompile Include="src\account.cpp" />
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\stdafx.cxx">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
+ <ClCompile Include="src\voip.cpp" />
<ClInclude Include="..\ExternalAPI\m_jingle.h" />
<ClInclude Include="src\account.h" />
<ClInclude Include="src\stdafx.h" />
diff --git a/plugins/Jingle/Jingle.vcxproj.filters b/plugins/Jingle/Jingle.vcxproj.filters
index 947797218d..edcb62535d 100644
--- a/plugins/Jingle/Jingle.vcxproj.filters
+++ b/plugins/Jingle/Jingle.vcxproj.filters
@@ -11,6 +11,12 @@
<ClCompile Include="src\account.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="src\voip.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="..\..\protocols\JabberG\src\jabber_xml.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\stdafx.h">
diff --git a/plugins/Jingle/src/account.cpp b/plugins/Jingle/src/account.cpp
index 551f59db00..a6e68b76ec 100644
--- a/plugins/Jingle/src/account.cpp
+++ b/plugins/Jingle/src/account.cpp
@@ -42,13 +42,250 @@ static int OnAccountCreated(WPARAM reason, LPARAM param)
return 0;
}
+static int OnSettingChanged(WPARAM hContact, LPARAM lParam)
+{
+ if (!hContact) {
+ auto *pcws = (DBCONTACTWRITESETTING *)lParam;
+ if (!mir_strcmp(pcws->szSetting, "EnableVOIP")) {
+ for (auto &it : g_arJabber) {
+ if (!mir_strcmp(it->m_szModuleName, pcws->szModule)) {
+ it->InitVoip(pcws->value.bVal != 0);
+ break;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
void CJabberAccount::InitHooks()
{
HookEvent(ME_SYSTEM_MODULESLOADED, &OnModulesLoaded);
HookEvent(ME_PROTO_ACCLISTCHANGED, &OnAccountCreated);
+ HookEvent(ME_DB_CONTACT_SETTINGCHANGED, &OnSettingChanged);
}
///////////////////////////////////////////////////////////////////////////////
+// Permanent IQ handler
+
+static BOOL OnProcessJingle(struct IJabberInterface *api, const TiXmlElement *node, void *pUserData)
+{
+ auto *pThis = (CJabberAccount *)pUserData;
+
+ auto *child = XmlGetChildByTag(node, "jingle", "xmlns", JABBER_FEAT_JINGLE);
+ if (!child)
+ return false;
+
+ const char *type = XmlGetAttr(node, "type");
+ if (type == nullptr)
+ return false;
+
+ const char *szAction = XmlGetAttr(child, "action");
+ const char *szSid = XmlGetAttr(child, "sid");
+
+ if (!mir_strcmp(type, "get") || !mir_strcmp(type, "set")) {
+ const char *idStr = XmlGetAttr(node, "id");
+ const char *from = XmlGetAttr(node, "from");
+ const char *szInitiator = XmlGetAttr(child, "initiator");
+ auto *content = XmlGetChildByTag(child, "content", "creator", "initiator");
+
+ if (szAction && szSid) {
+ if (!mir_strcmp(szAction, "session-initiate")) {
+ // if this is a Jingle 'session-initiate' and noone processed it yet, reply with "unsupported-applications"
+ api->SendXml(XmlNodeIq("result", idStr, from));
+
+ const TiXmlElement *descr = XmlGetChildByTag(content, "description", "xmlns", JABBER_FEAT_JINGLE_RTP);
+ const char *reason = NULL;
+ if (pThis->m_bEnableVOIP && descr) {
+ if (pThis->m_voipSession.IsEmpty()) {
+ pThis->m_voipSession = szSid;
+ pThis->m_voipPeerJid = from;
+ pThis->m_isOutgoing = false;
+ pThis->m_offerNode = child->DeepClone(&pThis->m_offerDoc)->ToElement();
+
+ //Make call GUI
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pThis->m_szModuleName;
+ vc.id = szSid; // Protocol specific ID for this call
+ vc.hContact = api->ContactFromJID(from); // Contact associated with the call (can be NULL)
+ vc.state = VOICE_STATE_RINGING;
+ vc.szNumber.a = pThis->m_voipPeerJid;
+ NotifyEventHooks(pThis->m_hVoiceEvent, WPARAM(&vc), 0);
+
+ // ringing message
+ XmlNodeIq iq("set", api->GetSerialNext(), from);
+ TiXmlElement *rjNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ rjNode << XATTR("action", "session-info") << XATTR("sid", szSid);
+ if (szInitiator)
+ rjNode << XATTR("initiator", szInitiator);
+ rjNode << XCHILDNS("ringing", "urn:xmpp:jingle:apps:rtp:info:1");
+
+ api->SendXml(iq);
+ return true;
+ }
+
+ // Save this event to history
+ PROTORECVEVENT recv = {};
+ recv.timestamp = (uint32_t)time(0);
+ recv.szMessage = "** A call while we were busy **";
+ ProtoChainRecvMsg(api->ContactFromJID(from), &recv);
+ reason = "busy";
+ }
+
+ XmlNodeIq iq("set", api->GetSerialNext(), from);
+ TiXmlElement *jingleNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ jingleNode << XATTR("action", "session-terminate") << XATTR("sid", szSid);
+ if (szInitiator)
+ jingleNode << XATTR("initiator", szInitiator);
+ jingleNode << XCHILD("reason") << XCHILD(reason ? reason : "unsupported-applications");
+
+ api->SendXml(iq);
+ return true;
+ }
+ else if (!mir_strcmp(szAction, "session-accept")) {
+ if (pThis->m_bEnableVOIP && pThis->m_voipSession == szSid) {
+ api->SendXml(XmlNodeIq("result", idStr, from));
+ if (pThis->OnRTPDescription(child)) {
+ //Make call GUI
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pThis->m_szModuleName;
+ vc.id = szSid;
+ vc.hContact = api->ContactFromJID(from);
+ vc.state = VOICE_STATE_TALKING;
+ NotifyEventHooks(pThis->m_hVoiceEvent, WPARAM(&vc), 0);
+ }
+ return true;
+ }
+ }
+ else if (!mir_strcmp(szAction, "session-terminate")) {
+ if (pThis->m_bEnableVOIP && pThis->m_voipSession == szSid) {
+ // EndCall()
+ api->SendXml(XmlNodeIq("result", idStr, from));
+
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pThis->m_szModuleName;
+ vc.id = szSid;
+ vc.hContact = api->ContactFromJID(from);
+ vc.state = VOICE_STATE_ENDED;
+ NotifyEventHooks(pThis->m_hVoiceEvent, WPARAM(&vc), 0);
+
+ pThis->VOIPTerminateSession(nullptr);
+ return true;
+ }
+ }
+ else if (!mir_strcmp(szAction, "transport-info")) {
+ auto *transport = XmlGetChildByTag(content, "transport", "xmlns", JABBER_FEAT_JINGLE_ICEUDP);
+ if (pThis->m_bEnableVOIP && pThis->m_voipSession == szSid && transport) {
+ api->SendXml(XmlNodeIq("result", idStr, from));
+ if (const TiXmlElement *candidate = XmlFirstChild(transport, "candidate")) {
+ pThis->OnICECandidate(candidate);
+ return true;
+ }
+ }
+ }
+ }
+
+ // if it's something else than 'session-initiate' and noone processed it yet, reply with "unknown-session"
+ XmlNodeIq iq("error", idStr, from);
+ TiXmlElement *errNode = iq << XCHILD("error");
+ errNode << XATTR("type", "cancel");
+ errNode << XCHILDNS("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
+ errNode << XCHILDNS("unknown-session", "urn:xmpp:jingle:errors:1");
+ api->SendXml(iq);
+ return true;
+ }
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Services
+
+static INT_PTR __cdecl JabberVOIP_call(void *pThis, WPARAM hContact, LPARAM)
+{
+ auto *pAcc = (CJabberAccount *)pThis;
+ if (pAcc->VOIPCallIinitiate(hContact)) {
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pAcc->m_szModuleName;
+ vc.id = pAcc->m_voipSession; // Protocol especific ID for this call
+ vc.hContact = hContact; // Contact associated with the call (can be NULL)
+ vc.state = VOICE_STATE_READY;
+ vc.szNumber.a = pAcc->m_voipPeerJid;
+ NotifyEventHooks(pAcc->m_hVoiceEvent, WPARAM(&vc), 0);
+ }
+
+ return 0;
+}
+
+static INT_PTR __cdecl JabberVOIP_answercall(void *pThis, WPARAM id, LPARAM)
+{
+ auto *pAcc = (CJabberAccount *)pThis;
+ if (strcmp((const char *)id, pAcc->m_voipSession))
+ return 0;
+
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pAcc->m_szModuleName;
+ vc.hContact = pAcc->m_api->ContactFromJID(pAcc->m_voipPeerJid);// Contact associated with the call (can be NULL)
+ vc.szNumber.a = pAcc->m_voipPeerJid;
+ vc.id = pAcc->m_voipSession;
+ vc.state = VOICE_STATE_ENDED;
+
+ if (pAcc->VOIPCreatePipeline()) {
+ if (pAcc->m_isOutgoing)
+ vc.state = VOICE_STATE_CALLING;
+ else if (pAcc->OnRTPDescription(pAcc->m_offerNode))
+ vc.state = VOICE_STATE_TALKING;
+ else
+ pAcc->VOIPTerminateSession();
+ }
+
+ NotifyEventHooks(pAcc->m_hVoiceEvent, WPARAM(&vc), 0);
+ return 0;
+}
+
+static INT_PTR __cdecl JabberVOIP_dropcall(void *pThis, WPARAM id, LPARAM)
+{
+ auto *pAcc = (CJabberAccount *)pThis;
+
+ VOICE_CALL vc = {};
+ vc.cbSize = sizeof(VOICE_CALL);
+ vc.moduleName = pAcc->m_szModuleName;
+ vc.id = (char *)id;
+ vc.state = VOICE_STATE_ENDED;
+ NotifyEventHooks(pAcc->m_hVoiceEvent, WPARAM(&vc), 0);
+
+ pAcc->VOIPTerminateSession();
+ return 0;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// CJabberAccount members
+
+CJabberAccount::CJabberAccount(IJabberInterface *_1) :
+ m_api(_1),
+ m_szModuleName(m_api->GetModuleName()),
+ m_bEnableVOIP(m_szModuleName, "EnableVOIP", false)
+{
+ CMStringA tmp(m_szModuleName);
+ m_hVoiceEvent = CreateHookableEvent(tmp + PE_VOICE_CALL_STATE);
+ CreateServiceFunctionObj(tmp + PS_VOICE_CALL, &JabberVOIP_call, this);
+ CreateServiceFunctionObj(tmp + PS_VOICE_ANSWERCALL, &JabberVOIP_answercall, this);
+ CreateServiceFunctionObj(tmp + PS_VOICE_DROPCALL, &JabberVOIP_dropcall, this);
+}
+
+CJabberAccount::~CJabberAccount()
+{
+ DestroyHookableEvent(m_hVoiceEvent);
+
+ if (m_bEnableVOIP)
+ InitVoip(false);
+}
void CJabberAccount::Init()
{
@@ -59,4 +296,9 @@ void CJabberAccount::Init()
m_api->RegisterFeature(JABBER_FEAT_JINGLE_RTPAUDIO, LPGEN("Jingle RTP Audio"));
m_api->AddFeatures(JABBER_FEAT_JINGLE "\0" JABBER_FEAT_JINGLE_ICEUDP "\0" JABBER_FEAT_JINGLE_RTP "\0" JABBER_FEAT_JINGLE_DTLS "\0" JABBER_FEAT_JINGLE_RTPAUDIO "\0\0");
+
+ m_api->AddIqHandler(&OnProcessJingle, JABBER_IQ_TYPE_ANY, JABBER_FEAT_JINGLE, 0, this);
+
+ if (m_bEnableVOIP)
+ InitVoip(true);
}
diff --git a/plugins/Jingle/src/account.h b/plugins/Jingle/src/account.h
index 8302f8e3f2..ae974b9363 100644
--- a/plugins/Jingle/src/account.h
+++ b/plugins/Jingle/src/account.h
@@ -1,16 +1,34 @@
#ifndef _ACCOUNT_H
#define _ACCOUNT_H
-struct CJabberAccount
+struct CJabberAccount : public MZeroedObject
{
- CJabberAccount(IJabberInterface *_1) :
- m_api(_1)
- {}
+ CJabberAccount(IJabberInterface *_1);
+ ~CJabberAccount();
IJabberInterface *m_api;
+ CMOption<bool> m_bEnableVOIP;
+
void Init();
static void InitHooks();
+
+ void InitVoip(bool bEnable);
+ bool OnICECandidate(const TiXmlElement *Node);
+ bool OnRTPDescription(const TiXmlElement *Node);
+ bool VOIPCreatePipeline();
+ bool VOIPTerminateSession(const char *reason = "cancel");
+ bool VOIPCallIinitiate(MCONTACT hContact);
+
+ const char *m_szModuleName;
+ CMStringA m_voipSession, m_voipPeerJid;
+ CMStringA m_voipICEPwd, m_voipICEUfrag, m_medianame;
+ bool m_isOutgoing = false;
+ TiXmlDocument m_offerDoc;
+ const TiXmlElement *m_offerNode = 0;
+ HANDLE m_hVoiceEvent = 0;
+ struct _GstElement *m_pipe1 = 0;
+ struct _GstElement *m_webrtc1 = 0;
};
extern OBJLIST<CJabberAccount> g_arJabber;
diff --git a/plugins/Jingle/src/main.cpp b/plugins/Jingle/src/main.cpp
index 016244ad29..bef4e15717 100644
--- a/plugins/Jingle/src/main.cpp
+++ b/plugins/Jingle/src/main.cpp
@@ -27,23 +27,10 @@ CMPlugin::CMPlugin() :
/////////////////////////////////////////////////////////////////////////////////////////
// Load
-static INT_PTR FakeService(WPARAM, LPARAM)
-{
- g_assertion_message(0, 0, 0, 0, 0);
- g_object_unref(0);
- gst_bin_add(0, 0);
- gst_rtp_header_extension_set_id(0, 0);
- gst_sdp_message_as_text(0);
- gst_webrtc_session_description_free(0);
- return 0;
-}
-
int CMPlugin::Load()
{
SetEnvironmentVariableW(L"GST_PLUGIN_PATH", VARSW(L"%miranda_path%\\Libs\\gst_plugins"));
- CreateServiceFunction("JINGLE/SERVICE", &FakeService);
-
CJabberAccount::InitHooks();
return 0;
}
diff --git a/plugins/Jingle/src/stdafx.h b/plugins/Jingle/src/stdafx.h
index ff0b86c4b9..4c7a794c1a 100644
--- a/plugins/Jingle/src/stdafx.h
+++ b/plugins/Jingle/src/stdafx.h
@@ -26,6 +26,8 @@
#include <m_voice.h>
#include <m_voiceservice.h>
+#include "../../protocols/JabberG/src/jabber_xml.h"
+
#include "account.h"
#include "resource.h"
#include "version.h"
diff --git a/plugins/Jingle/src/voip.cpp b/plugins/Jingle/src/voip.cpp
new file mode 100644
index 0000000000..bd5e541499
--- /dev/null
+++ b/plugins/Jingle/src/voip.cpp
@@ -0,0 +1,525 @@
+#include "stdafx.h"
+
+#include <m_voiceservice.h>
+
+#include <gst/gst.h>
+#include <gst/sdp/sdp.h>
+#include <gst/rtp/rtp.h>
+
+#define GST_USE_UNSTABLE_API
+#include <gst/webrtc/webrtc.h>
+
+bool GetCandidateProp(char *output, byte maxlen, const char *candidate, const char *prop)
+{
+ const char *pprop = strstr(candidate, prop);
+ if (!pprop)
+ return false;
+
+ const char *val = pprop + strlen(prop);
+ while (*val == ' ') val++;
+ int i = 0;
+ while (*val != 0 && *val != ' ' && i < maxlen - 1)
+ output[i++] = *val++;
+ output[i] = 0;
+
+ return i > 0;
+}
+
+static void handle_media_stream(GstPad *pad, GstElement *pipe, const char *convert_name, const char *sink_name)
+{
+ GstPad *qpad;
+ GstElement *q, *conv, *resample, *sink;
+ GstPadLinkReturn ret;
+
+ gst_print("Trying to handle stream with %s ! %s", convert_name, sink_name);
+
+ q = gst_element_factory_make("queue", NULL);
+ g_assert_nonnull(q);
+ conv = gst_element_factory_make(convert_name, NULL);
+ g_assert_nonnull(conv);
+ sink = gst_element_factory_make(sink_name, NULL);
+ g_assert_nonnull(sink);
+
+ if (g_strcmp0(convert_name, "audioconvert") == 0) {
+ /* Might also need to resample, so add it just in case.
+ * Will be a no-op if it's not required. */
+ resample = gst_element_factory_make("audioresample", NULL);
+ g_assert_nonnull(resample);
+ gst_bin_add_many(GST_BIN(pipe), q, conv, resample, sink, NULL);
+ gst_element_sync_state_with_parent(q);
+ gst_element_sync_state_with_parent(conv);
+ gst_element_sync_state_with_parent(resample);
+ gst_element_sync_state_with_parent(sink);
+ gst_element_link_many(q, conv, resample, sink, NULL);
+ }
+ else {
+ gst_bin_add_many(GST_BIN(pipe), q, conv, sink, NULL);
+ gst_element_sync_state_with_parent(q);
+ gst_element_sync_state_with_parent(conv);
+ gst_element_sync_state_with_parent(sink);
+ gst_element_link_many(q, conv, sink, NULL);
+ }
+
+ qpad = gst_element_get_static_pad(q, "sink");
+
+ ret = gst_pad_link(pad, qpad);
+ g_assert_cmphex(ret, == , GST_PAD_LINK_OK);
+}
+
+static void on_incoming_decodebin_stream(GstElement * /*decodebin*/, GstPad *pad, GstElement *pipe)
+{
+ GstCaps *caps;
+ const gchar *name;
+
+ if (!gst_pad_has_current_caps(pad)) {
+ gst_printerr("Pad '%s' has no caps, can't do anything, ignoring\n", GST_PAD_NAME(pad));
+ return;
+ }
+
+ caps = gst_pad_get_current_caps(pad);
+ name = gst_structure_get_name(gst_caps_get_structure(caps, 0));
+
+ if (g_str_has_prefix(name, "video")) {
+ handle_media_stream(pad, pipe, "videoconvert", "autovideosink");
+ }
+ else if (g_str_has_prefix(name, "audio")) {
+ handle_media_stream(pad, pipe, "audioconvert", "autoaudiosink");
+ }
+ else {
+ gst_printerr("Unknown pad %s, ignoring", GST_PAD_NAME(pad));
+ }
+}
+
+static void on_incoming_stream_cb(GstElement */*webrtc*/, GstPad *pad, GstElement *pipe)
+{
+ GstElement *decodebin;
+ GstPad *sinkpad;
+
+ if (GST_PAD_DIRECTION(pad) != GST_PAD_SRC)
+ return;
+
+ decodebin = gst_element_factory_make("decodebin", NULL);
+ g_signal_connect(decodebin, "pad-added", G_CALLBACK(on_incoming_decodebin_stream), pipe);
+ gst_bin_add(GST_BIN(pipe), decodebin);
+ gst_element_sync_state_with_parent(decodebin);
+
+ sinkpad = gst_element_get_static_pad(decodebin, "sink");
+ gst_pad_link(pad, sinkpad);
+ gst_object_unref(sinkpad);
+}
+
+void on_offer_created_cb(GstPromise *promise, gpointer user_data)
+{
+ GstWebRTCSessionDescription *offer = NULL;
+ CJabberAccount *jproto = (CJabberAccount *)user_data;
+
+ GstStructure const *reply = gst_promise_get_reply(promise);
+ gst_structure_get(reply, jproto->m_isOutgoing ? "offer" : "answer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
+ gst_promise_unref(promise);
+ if (!offer) {
+ gst_print("Cannot process sdp");
+ return;
+ }
+
+ GstPromise *local_desc_promise = gst_promise_new();
+ g_signal_emit_by_name(jproto->m_webrtc1, "set-local-description", offer, local_desc_promise);
+ gst_promise_interrupt(local_desc_promise);
+ gst_promise_unref(local_desc_promise);
+
+ gchar *sdp_string = gst_sdp_message_as_text(offer->sdp);
+ gst_print("VOIP - Wanna send SDP offer:\r\n%s\r\n", sdp_string);
+ g_free(sdp_string);
+
+ const GstSDPMedia *media_audio = NULL;
+ for (unsigned int i = 0; i < gst_sdp_message_medias_len(offer->sdp); i++) {
+ const GstSDPMedia *m = gst_sdp_message_get_media(offer->sdp, i);
+ if (!strcmp(m->media, "audio"))
+ media_audio = m;
+ }
+ if (!media_audio) {
+ gst_print("No audio media in SDP");
+ return;
+ }
+
+ jproto->m_voipICEPwd = gst_sdp_media_get_attribute_val(media_audio, "ice-pwd");
+ jproto->m_voipICEUfrag = gst_sdp_media_get_attribute_val(media_audio, "ice-ufrag");
+ jproto->m_medianame = gst_sdp_media_get_attribute_val(media_audio, "mid");
+
+ // send it all
+ bool outgoing = jproto->m_isOutgoing;
+ XmlNodeIq iq("set", jproto->m_api->GetSerialNext(), jproto->m_voipPeerJid);
+ TiXmlElement *rjNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ rjNode << XATTR("sid", jproto->m_voipSession)
+ << XATTR("action", outgoing ? "session-initiate" : "session-accept")
+ << XATTR("initiator", outgoing ? jproto->m_api->GetFullJid() : jproto->m_voipPeerJid);
+ if (!outgoing)
+ rjNode << XATTR("responder", jproto->m_api->GetFullJid());
+
+ TiXmlElement *content = rjNode << XCHILD("content") << XATTR("creator", "initiator") << XATTR("name", jproto->m_medianame);
+ TiXmlElement *description = content << XCHILDNS("description", JABBER_FEAT_JINGLE_RTP) << XATTR("media", "audio");
+
+ auto *opuspayload = description << XCHILD("payload-type") << XATTR("id", "111") << XATTR("name", "opus") << XATTR("clockrate", "48000") << XATTR("channels", "2");
+
+ opuspayload << XCHILD("parameter") << XATTR("name", "minptime") << XATTR("value", "10");
+ opuspayload << XCHILD("parameter") << XATTR("name", "useinbandfec") << XATTR("value", "1");
+ opuspayload << XCHILDNS("rtcp-fb", "urn:xmpp:jingle:apps:rtp:rtcp-fb:0") << XATTR("type", "transport-cc");
+
+ description << XCHILDNS("rtp-hdrext", "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0") << XATTR("id", "1") << XATTR("uri", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01");
+ /*
+ auto* source = description << XCHILDNS("source", "urn:xmpp:jingle:apps:rtp:ssma:0") << XATTR("ssrc", "2165039095");
+ source << XCHILD("parameter") << XATTR("name", "cname") << XATTR("value", "8ee+PcGu8BNwq22f");
+ source << XCHILD("parameter") << XATTR("name", "msid") << XATTR("value", "my-media-stream2 my-audio-track2");
+ source << XCHILD("parameter") << XATTR("name", "mslabel") << XATTR("value", "my-media-stream2");
+ source << XCHILD("parameter") << XATTR("name", "label") << XATTR("value", "my-audio-track2");*/
+
+ description << XCHILD("rtcp-mux");
+
+ //fingerprint
+ char hash[100];
+ if (sscanf(gst_sdp_media_get_attribute_val(media_audio, "fingerprint"), "sha-256 %95s", hash) == 1) {
+ auto *transport = content << XCHILDNS("transport", JABBER_FEAT_JINGLE_ICEUDP);
+ transport << XATTR("pwd", jproto->m_voipICEPwd) << XATTR("ufrag", jproto->m_voipICEUfrag);
+
+ auto *fingerprint = transport << XCHILD("fingerprint", hash);
+ fingerprint << XATTR("xmlns", JABBER_FEAT_JINGLE_DTLS) << XATTR("hash", "sha-256")
+ << XATTR("setup", gst_sdp_media_get_attribute_val(media_audio, "setup"));
+ }
+
+ jproto->m_api->SendXml(iq);
+
+ gst_webrtc_session_description_free(offer);
+}
+
+void on_negotiation_needed_cb(GstElement *webrtcbin, gpointer user_data)
+{
+ if (((CJabberAccount *)user_data)->m_isOutgoing) {
+ gst_print("Creating negotiation offer\n");
+
+ GstPromise *promise = gst_promise_new_with_change_func(on_offer_created_cb, user_data, NULL);
+ g_signal_emit_by_name(G_OBJECT(webrtcbin), "create-offer", NULL, promise);
+ }
+}
+
+static void on_offer_set(GstPromise *promise, gpointer user_data)
+{
+ gst_promise_unref(promise);
+ promise = gst_promise_new_with_change_func(on_offer_created_cb, user_data, NULL);
+ g_signal_emit_by_name(((CJabberAccount *)user_data)->m_webrtc1, "create-answer", NULL, promise);
+}
+
+void send_ice_candidate_message_cb(G_GNUC_UNUSED GstElement */*webrtcbin*/, guint mline_index, gchar *candidate, CJabberAccount *jproto)
+{
+ // parse candidate and send
+ char foundation[11], component[11], protocol[4] = "", priority[11], ip[40], port[6], type[6];
+ int ret = sscanf(candidate, "candidate:%10s %10s %3s %10s %39s %5s typ %5s",
+ foundation, component, protocol, priority, ip, port, type);
+ if (ret != 7 || strcmp(protocol, "UDP"))
+ return;
+
+ gst_print("VOIP - Wanna send ice candidate(m-line_index=%d):\r\n%s\r\n", mline_index, candidate);
+ for (char *p = protocol; *p; ++p) *p = tolower(*p);
+
+ XmlNodeIq iq("set", jproto->m_api->GetSerialNext(), jproto->m_voipPeerJid);
+ TiXmlElement *rjNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ rjNode << XATTR("action", "transport-info") << XATTR("sid", jproto->m_voipSession);
+
+ TiXmlElement *content = rjNode << XCHILD("content");
+ content << XATTR("creator", "initiator") << XATTR("name", jproto->m_medianame);
+
+ auto *transport = content << XCHILDNS("transport", JABBER_FEAT_JINGLE_ICEUDP);
+ transport << XATTR("pwd", jproto->m_voipICEPwd) << XATTR("ufrag", jproto->m_voipICEUfrag);
+
+ auto *candidateNode = transport << XCHILD("candidate");
+ candidateNode << XATTR("type", type) << XATTR("protocol", protocol) << XATTR("ip", ip)
+ << XATTR("port", port) << XATTR("priority", priority) << XATTR("foundation", foundation) << XATTR("component", component);
+
+ char attr[255];
+ if (GetCandidateProp(attr, 255, candidate, "raddr"))
+ candidateNode << XATTR("rel-addr", attr);
+ if (GetCandidateProp(attr, 255, candidate, "rport"))
+ candidateNode << XATTR("rel-port", attr);
+
+ jproto->m_api->SendXml(iq);
+}
+
+static gboolean check_plugins(void)
+{
+ const gchar *needed[] = { "opus", "nice", "webrtc", "dtls", "srtp", "rtpmanager"
+ /*"vpx", "videotestsrc", "audiotestsrc",*/ };
+
+ GstRegistry *registry = gst_registry_get();
+ gboolean ret = TRUE;
+ for (auto &it : needed) {
+ GstPlugin *plugin = gst_registry_find_plugin(registry, it);
+ if (!plugin) {
+ gst_print("Required gstreamer plugin '%s' not found\n", it);
+ ret = FALSE;
+ }
+ else gst_object_unref(plugin);
+ }
+
+ return ret;
+}
+
+void dbgprint(const gchar *string)
+{
+ OutputDebugStringA(string);
+}
+
+bool CJabberAccount::VOIPCreatePipeline(void)
+{
+ if (!m_bEnableVOIP)
+ goto err;
+
+ //gstreamer init
+ static bool gstinited = 0;
+ if (!gstinited) {
+ if (!LoadLibrary(L"gstreamer-1.0-0.dll")) {
+ MessageBoxA(0, "Cannot load Gstreamer library!", 0, MB_OK | MB_ICONERROR);
+ goto err;
+ }
+ gst_init(NULL, NULL);
+ g_set_print_handler(dbgprint);
+ gst_print("preved medved");
+ if (!check_plugins()) {
+ MessageBoxA(0, "Gstreamer plugins not found!", 0, MB_OK | MB_ICONERROR);
+ goto err;
+ }
+ gstinited = 1;
+ }
+
+ #define STUN_SERVER "stun-server=stun://stun.tng.de:3478 "
+ #define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
+ #define RTP_TWCC_URI "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
+
+ GError *error = NULL;
+ m_pipe1 = gst_parse_launch(
+ "webrtcbin bundle-policy=max-bundle name=sendrecv "
+ STUN_SERVER
+ "autoaudiosrc ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay name=audiopay ! "
+ "queue ! " RTP_CAPS_OPUS "111 ! sendrecv. ", &error);
+
+ if (error) {
+ MessageBoxA(0, "Failed to parse launch: ", error->message, MB_OK);
+ g_error_free(error);
+ goto err;
+ }
+
+ m_webrtc1 = gst_bin_get_by_name(GST_BIN(m_pipe1), "sendrecv");
+ g_assert_nonnull(m_webrtc1);
+ if (!m_webrtc1)
+ goto err;
+
+ GstElement *audiopay = gst_bin_get_by_name(GST_BIN(m_pipe1), "audiopay");
+ g_assert_nonnull(audiopay);
+ GstRTPHeaderExtension *audio_twcc = gst_rtp_header_extension_create_from_uri(RTP_TWCC_URI);
+ g_assert_nonnull(audio_twcc);
+ gst_rtp_header_extension_set_id(audio_twcc, 1);
+ g_signal_emit_by_name(audiopay, "add-extension", audio_twcc);
+ g_clear_object(&audio_twcc);
+ g_clear_object(&audiopay);
+
+ // It will be called when the pipeline goes to PLAYING.
+ g_signal_connect(m_webrtc1, "on-negotiation-needed", G_CALLBACK(on_negotiation_needed_cb), this);
+ // It will be called when we obtain local ICE candidate
+ g_signal_connect(m_webrtc1, "on-ice-candidate", G_CALLBACK(send_ice_candidate_message_cb), this);
+ // idk
+ g_signal_connect(m_webrtc1, "pad-added", G_CALLBACK(on_incoming_stream_cb), m_pipe1);
+
+ // Lifetime is the same as the pipeline itself
+ gst_object_unref(m_webrtc1);
+
+ gst_print("Starting pipeline\n");
+ if (gst_element_set_state(GST_ELEMENT(m_pipe1), GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE)
+ return true;
+
+err:
+ VOIPTerminateSession();
+ return false;
+}
+
+bool CJabberAccount::VOIPTerminateSession(const char *reason)
+{
+ if (m_pipe1) {
+ gst_element_set_state(GST_ELEMENT(m_pipe1), GST_STATE_NULL);
+ g_clear_object(&m_pipe1);
+ gst_object_unref(m_pipe1);
+ gst_print("Pipeline stopped\n");
+ }
+
+ if (reason && !m_voipSession.IsEmpty() && !m_voipPeerJid.IsEmpty()) {
+ XmlNodeIq iq("set", m_api->GetSerialNext(), m_voipPeerJid);
+
+ TiXmlElement *jingleNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ jingleNode << XATTR("action", "session-terminate") << XATTR("sid", m_voipSession);
+ jingleNode << XATTR("initiator", m_isOutgoing ? m_api->GetFullJid() : m_voipPeerJid);
+ jingleNode << XCHILD("reason") << XCHILD(reason);
+
+ m_api->SendXml(iq);
+ }
+
+ m_voipICEPwd.Empty();
+ m_voipICEUfrag.Empty();
+ m_medianame.Empty();
+
+ m_voipSession.Empty();
+ m_voipPeerJid.Empty();
+ m_pipe1 = m_webrtc1 = NULL;
+ return true;
+}
+
+bool CJabberAccount::OnRTPDescription(const TiXmlElement *jingleNode)
+{
+ if (!jingleNode)
+ return false;
+
+ // process remote offer
+ auto *content = XmlGetChildByTag(jingleNode, "content", "creator", "initiator");
+ auto *transport = XmlGetChildByTag(content, "transport", "xmlns", "urn:xmpp:jingle:transports:ice-udp:1");
+ auto *description = XmlGetChildByTag(content, "description", "xmlns", "urn:xmpp:jingle:apps:rtp:1");
+ auto *source = XmlGetChildByTag(description, "source", "xmlns", "urn:xmpp:jingle:apps:rtp:ssma:0");
+
+ CMStringA sdp_string(FORMAT, "v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=ice-options:trickle\r\n"
+ "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:%s\r\na=ice-pwd:%s\r\na=rtcp-mux\r\na=sendrecv\r\na=rtpmap:111 OPUS/48000/2\r\n"
+
+ "a=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\n"
+ "a=ssrc:%s msid:%s\r\n"
+ "a=ssrc:%s cname:%s\r\n"
+
+ "a=mid:%s\r\na=setup:%s\r\na=fingerprint:sha-256 %s\r\na=rtcp-mux-only\r\n",
+ XmlGetAttr(transport, "ufrag"),
+ XmlGetAttr(transport, "pwd"),
+
+ XmlGetAttr(source, "ssrc"),
+ XmlGetAttr(XmlGetChildByTag(source, "parameter", "name", "msid"), "value"),
+ XmlGetAttr(source, "ssrc"),
+ XmlGetAttr(XmlGetChildByTag(source, "parameter", "name", "cname"), "value"),
+
+ XmlGetAttr(content, "name"),
+ XmlGetAttr(XmlFirstChild(transport, "fingerprint"), "setup"),
+ XmlFirstChild(transport, "fingerprint")->GetText());
+
+ GstSDPMessage *sdp;
+ int ret = gst_sdp_message_new(&sdp);
+ g_assert_cmphex(ret, == , GST_SDP_OK);
+ ret = gst_sdp_message_parse_buffer((guint8 *)sdp_string.c_str(), sdp_string.GetLength(), sdp);
+ if (ret != GST_SDP_OK) {
+ g_error("Could not parse SDP string\n");
+ return false;
+ }
+
+ gchar *str = gst_sdp_message_as_text(sdp);
+ gst_print("VOIP - Eating remote SDP offer:\r\n%s\r\n", str);
+ g_free(str);
+
+ if (m_isOutgoing) {
+ GstWebRTCSessionDescription *answer = gst_webrtc_session_description_new(GST_WEBRTC_SDP_TYPE_ANSWER, sdp);
+ g_assert_nonnull(answer);
+
+ GstPromise *promise = gst_promise_new();
+ g_signal_emit_by_name(m_webrtc1, "set-remote-description", answer, promise);
+ gst_promise_interrupt(promise);
+ gst_promise_unref(promise);
+ gst_webrtc_session_description_free(answer);
+ }
+ else {
+ // Set remote description on our pipeline
+ GstWebRTCSessionDescription *offer = gst_webrtc_session_description_new(GST_WEBRTC_SDP_TYPE_OFFER, sdp);
+ g_assert_nonnull(offer);
+
+ GstPromise *promise = gst_promise_new_with_change_func(on_offer_set, this, NULL);
+ g_signal_emit_by_name(m_webrtc1, "set-remote-description", offer, promise);
+ gst_webrtc_session_description_free(offer);
+ }
+
+ return true;
+}
+
+bool CJabberAccount::OnICECandidate(const TiXmlElement *Node)
+{
+ if (!m_bEnableVOIP)
+ return false;
+
+ CMStringA scandidate;
+ CMStringA proto(XmlGetAttr(Node, "protocol"));
+ proto.MakeUpper();
+
+ scandidate.AppendFormat("candidate:%s ", XmlGetAttr(Node, "foundation")); //FIXME
+ scandidate.AppendFormat("%s ", XmlGetAttr(Node, "component"));
+ scandidate.AppendFormat("%s ", proto.c_str());
+ scandidate.AppendFormat("%s ", XmlGetAttr(Node, "priority"));
+ scandidate.AppendFormat("%s ", XmlGetAttr(Node, "ip"));
+ scandidate.AppendFormat("%s ", XmlGetAttr(Node, "port"));
+ scandidate.AppendFormat("typ %s", XmlGetAttr(Node, "type"));
+
+ if (const char *tmp = XmlGetAttr(Node, "rel-addr"))
+ scandidate.AppendFormat(" raddr %s", tmp);
+ if (const char *tmp = XmlGetAttr(Node, "rel-port"))
+ scandidate.AppendFormat(" rport %s", tmp);
+ if (const char *generation = XmlGetAttr(Node, "generation"))
+ scandidate.AppendFormat(" generation %s", generation);
+
+ gst_print("VOIP - Accepting ICE candidate:\r\n%s\r\n", scandidate.c_str());
+ g_signal_emit_by_name(m_webrtc1, "add-ice-candidate", 0, scandidate.c_str());
+ return true;
+}
+
+bool CJabberAccount::VOIPCallIinitiate(MCONTACT hContact)
+{
+ if (!m_voipSession.IsEmpty()) {
+ VOIPTerminateSession();
+ MessageBoxA(0, "Something went wrong\r\nOld session terminated", NULL, 0);
+ return false;
+ }
+
+ if (!m_bEnableVOIP)
+ return false;
+
+ CMStringA jid(db_get_sm(hContact, m_szModuleName, "jid"));
+ if (jid.IsEmpty())
+ return false;
+
+ ptrA szResource(m_api->GetBestResourceName(jid));
+ if (szResource) {
+ jid.AppendFormat("/%s", szResource.get());
+ bool bFound = false;
+ ptrA szFeatures(m_api->GetResourceFeatures(jid));
+ for (auto *p = szFeatures.get(); *p; p += mir_strlen(p))
+ if (!mir_strcmp(p, JABBER_FEAT_JINGLE))
+ bFound = true;
+
+ if (!bFound) {
+ // MsgPopup(hContact, TranslateT("Client's program does not support voice calls"), TranslateT("Error"));
+ return false;
+ }
+ }
+
+ unsigned char tmp[16];
+ Utils_GetRandom(tmp, sizeof(tmp));
+
+ m_isOutgoing = true;
+ m_voipSession = ptrA(mir_base64_encode(tmp, sizeof(tmp)));
+ m_voipPeerJid = jid.c_str();
+
+ return true;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// module entry point
+
+void CJabberAccount::InitVoip(bool bEnable)
+{
+ // Voip
+ VOICE_MODULE vsr = {};
+ vsr.cbSize = sizeof(VOICE_MODULE);
+ vsr.description = L"XMPP/DTLS-SRTP";
+ vsr.name = (char*)m_szModuleName;
+ vsr.icon = g_plugin.getIconHandle(IDI_MAIN);
+ vsr.flags = 3;
+ if (bEnable)
+ CallService(MS_VOICESERVICE_REGISTER, (WPARAM)&vsr, 0);
+ else {
+ VOIPTerminateSession();
+ CallService(MS_VOICESERVICE_UNREGISTER, (WPARAM)&vsr, 0);
+ }
+}