diff options
author | George Hazan <ghazan@miranda.im> | 2022-08-03 21:02:36 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2022-08-03 21:02:36 +0300 |
commit | 5323a782c4e8c42781f22ce2f488962a18f82554 (patch) | |
tree | f71537197b16f0f8fd0d6937f7120d018d220814 /protocols | |
parent | 50acf9d37183f86f6f623aad410003392b0af41f (diff) |
Jabber: initial version of Jingle support
Diffstat (limited to 'protocols')
-rw-r--r-- | protocols/JabberG/jabber.vcxproj | 9 | ||||
-rw-r--r-- | protocols/JabberG/jabber.vcxproj.filters | 3 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_caps.cpp | 8 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_caps.h | 9 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_menu.cpp | 17 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_opt.cpp | 3 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_proto.cpp | 1 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_proto.h | 16 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_thread.cpp | 97 | ||||
-rw-r--r-- | protocols/JabberG/src/jabber_voip.cpp | 519 |
10 files changed, 649 insertions, 33 deletions
diff --git a/protocols/JabberG/jabber.vcxproj b/protocols/JabberG/jabber.vcxproj index 4d882b075d..cffe651bac 100644 --- a/protocols/JabberG/jabber.vcxproj +++ b/protocols/JabberG/jabber.vcxproj @@ -25,6 +25,11 @@ <ImportGroup Label="PropertySheets">
<Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" />
</ImportGroup>
+ <ItemDefinitionGroup>
+ <ClCompile>
+ <AdditionalIncludeDirectories>..\..\include\glib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+ </ClCompile>
+ </ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\jabber.cpp" />
<ClCompile Include="src\jabber_adhoc.cpp" />
@@ -75,6 +80,7 @@ <ClCompile Include="src\jabber_userinfo.cpp" />
<ClCompile Include="src\jabber_util.cpp" />
<ClCompile Include="src\jabber_vcard.cpp" />
+ <ClCompile Include="src\jabber_voip.cpp" />
<ClCompile Include="src\jabber_xml.cpp" />
<ClCompile Include="src\jabber_xstatus.cpp" />
<ClCompile Include="src\jabber_zstream.cpp" />
@@ -107,7 +113,8 @@ </ItemGroup>
<ItemDefinitionGroup>
<Link>
- <AdditionalDependencies>libcrypto.lib;libssl.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ <AdditionalDependencies>delayimp.lib;libcrypto.lib;libssl.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ <DelayLoadDLLs>glib-2.0-0.dll;gobject-2.0-0.dll;gstreamer-1.0-0.dll;gstrtp-1.0-0.dll;gstsdp-1.0-0.dll;gstwebrtc-1.0-0.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
diff --git a/protocols/JabberG/jabber.vcxproj.filters b/protocols/JabberG/jabber.vcxproj.filters index 30d426c7d2..7894068683 100644 --- a/protocols/JabberG/jabber.vcxproj.filters +++ b/protocols/JabberG/jabber.vcxproj.filters @@ -161,6 +161,9 @@ <ClCompile Include="src\jabber_mam.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="src\jabber_voip.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\jabber_byte.h">
diff --git a/protocols/JabberG/src/jabber_caps.cpp b/protocols/JabberG/src/jabber_caps.cpp index 4b6fada073..e49f93d8a8 100644 --- a/protocols/JabberG/src/jabber_caps.cpp +++ b/protocols/JabberG/src/jabber_caps.cpp @@ -78,7 +78,11 @@ const JabberFeatCapPair g_JabberFeatCapPairs[] = { JABBER_FEAT_ROSTER_EXCHANGE, JABBER_CAPS_ROSTER_EXCHANGE, LPGEN("Supports Roster Exchange") },
{ JABBER_FEAT_DIRECT_MUC_INVITE, JABBER_CAPS_DIRECT_MUC_INVITE, LPGEN("Supports direct chat invitations (XEP-0249)") },
{ JABBER_FEAT_OMEMO_DEVICELIST_NOTIFY, JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY, LPGEN("Receives information about OMEMO devices") },
- { JABBER_FEAT_CARBONS, JABBER_CAPS_CARBONS, LPGEN("Supports message carbons (XEP-0280)")},
+ { JABBER_FEAT_CARBONS, JABBER_CAPS_CARBONS, LPGEN("Supports message carbons (XEP-0280)")},
+ { JABBER_FEAT_JINGLE_ICEUDP, JABBER_CAPS_JINGLE_ICEUDP, LPGEN("Jingle ICE-UDP Transport") },
+ { JABBER_FEAT_JINGLE_RTP, JABBER_CAPS_JINGLE_RTP, LPGEN("Jingle RTP") },
+ { JABBER_FEAT_JINGLE_DTLS, JABBER_CAPS_JINGLE_DTLS, LPGEN("Jingle DTLS") },
+ { JABBER_FEAT_JINGLE_RTPAUDIO, JABBER_CAPS_JINGLE_RTPAUDIO, LPGEN("Jingle RTP Audio") },
};
const int g_cJabberFeatCapPairs = _countof(g_JabberFeatCapPairs);
@@ -347,6 +351,8 @@ JabberCapsBits CJabberProto::GetOwnCaps(bool IncludeDynamic) jcb |= JABBER_CAPS_OMEMO_DEVICELIST_NOTIFY;
if (!m_bMsgAck)
jcb &= ~(JABBER_CAPS_CHAT_MARKERS | JABBER_CAPS_MESSAGE_RECEIPTS);
+ if (m_bEnableVOIP)
+ jcb |= JABBER_CAPS_JINGLE | JABBER_CAPS_JINGLE_ICEUDP | JABBER_CAPS_JINGLE_RTP | JABBER_CAPS_JINGLE_DTLS | JABBER_CAPS_JINGLE_RTPAUDIO;
return jcb;
}
diff --git a/protocols/JabberG/src/jabber_caps.h b/protocols/JabberG/src/jabber_caps.h index 76313d77aa..fc37993fb0 100644 --- a/protocols/JabberG/src/jabber_caps.h +++ b/protocols/JabberG/src/jabber_caps.h @@ -195,6 +195,15 @@ typedef unsigned __int64 JabberCapsBits; #define JABBER_FEAT_BITS "urn:xmpp:bob"
#define JABBER_CAPS_BITS ((JabberCapsBits)1<<50)
+#define JABBER_FEAT_JINGLE_ICEUDP "urn:xmpp:jingle:transports:ice-udp:1"
+#define JABBER_CAPS_JINGLE_ICEUDP ((JabberCapsBits)1<<51)
+#define JABBER_FEAT_JINGLE_RTP "urn:xmpp:jingle:apps:rtp:1"
+#define JABBER_CAPS_JINGLE_RTP ((JabberCapsBits)1<<52)
+#define JABBER_FEAT_JINGLE_DTLS "urn:xmpp:jingle:apps:dtls:0"
+#define JABBER_CAPS_JINGLE_DTLS ((JabberCapsBits)1<<53)
+#define JABBER_FEAT_JINGLE_RTPAUDIO "urn:xmpp:jingle:apps:rtp:audio"
+#define JABBER_CAPS_JINGLE_RTPAUDIO ((JabberCapsBits)1<<54)
+
#define JABBER_FEAT_ARCHIVE "urn:xmpp:archive"
#define JABBER_FEAT_BIND "urn:ietf:params:xml:ns:xmpp-bind"
#define JABBER_FEAT_CAPTCHA "urn:xmpp:captcha"
diff --git a/protocols/JabberG/src/jabber_menu.cpp b/protocols/JabberG/src/jabber_menu.cpp index f37afe8eb4..808302b164 100644 --- a/protocols/JabberG/src/jabber_menu.cpp +++ b/protocols/JabberG/src/jabber_menu.cpp @@ -126,6 +126,13 @@ static int JabberPrebuildContactMenu(WPARAM hContact, LPARAM lParam) return(ppro) ? ppro->OnPrebuildContactMenu(hContact, lParam) : 0;
}
+static INT_PTR JabberVOIPCallIinitiate(WPARAM hContact, LPARAM)
+{
+ CJabberProto *ppro = CMPlugin::getInstance(hContact);
+ ppro->VOIPCallIinitiate(hContact);
+ return 0;
+}
+
void g_MenuInit(void)
{
hStatusMenuInit = CreateHookableEvent(ME_JABBER_MENUINIT);
@@ -245,6 +252,16 @@ void g_MenuInit(void) mi.hIcolibItem = g_plugin.getIconHandle(IDI_NODE_SERVER);
g_hMenuResourcesServer = Menu_AddContactMenuItem(&mi);
CreateServiceFunctionParam(mi.pszService, JabberMenuHandleResource, MENUITEM_SERVER);
+
+ SET_UID(mi, 0x2fe60fc5, 0x6417, 0x4f37, 0xa0, 0xbe, 0xa0, 0x59, 0x94, 0x11, 0x1d, 0xd9);
+ mi.root = nullptr;
+ mi.flags = 0;
+ mi.hIcolibItem = g_plugin.getIconHandle(IDI_NODE_RSS);
+ mi.name.a = LPGEN("Voice call");
+ mi.pszService = "Jabber/VOIPCallIinitiate";
+ mi.position = -1999902010;
+ Menu_AddContactMenuItem(&mi);
+ CreateServiceFunction(mi.pszService, JabberVOIPCallIinitiate);
}
void g_MenuUninit(void)
diff --git a/protocols/JabberG/src/jabber_opt.cpp b/protocols/JabberG/src/jabber_opt.cpp index 00313c3d5d..c23c101555 100644 --- a/protocols/JabberG/src/jabber_opt.cpp +++ b/protocols/JabberG/src/jabber_opt.cpp @@ -706,8 +706,9 @@ public: m_options.AddOption(LPGENW("Messaging"), LPGENW("Enable server-side history (XEP-0136)"), m_proto->m_bEnableMsgArchive);
m_options.AddOption(LPGENW("Messaging"), LPGENW("Enable Message Archive Management (XEP-0313)"), m_proto->m_bEnableMam);
m_options.AddOption(LPGENW("Messaging"), LPGENW("Enable carbon copies (XEP-0280)"), m_proto->m_bEnableCarbons);
- m_options.AddOption(LPGENW("Messaging"), LPGENW("Use Stream Management (XEP-0198) if possible (Testing)"), m_proto->m_bEnableStreamMgmt);
+ m_options.AddOption(LPGENW("Messaging"), LPGENW("Enable VOIP(for developers)"), m_proto->m_bEnableVOIP);
+ m_options.AddOption(LPGENW("Server options"), LPGENW("Use Stream Management (XEP-0198) if possible (Testing)"), m_proto->m_bEnableStreamMgmt);
m_options.AddOption(LPGENW("Server options"), LPGENW("Disable SASL authentication (for old servers)"), m_proto->m_bDisable3920auth);
m_options.AddOption(LPGENW("Server options"), LPGENW("Enable stream compression (if possible)"), m_proto->m_bEnableZlib);
diff --git a/protocols/JabberG/src/jabber_proto.cpp b/protocols/JabberG/src/jabber_proto.cpp index 946ae533f4..cdae4a4df3 100644 --- a/protocols/JabberG/src/jabber_proto.cpp +++ b/protocols/JabberG/src/jabber_proto.cpp @@ -101,6 +101,7 @@ CJabberProto::CJabberProto(const char *aProtoName, const wchar_t *aUserName) : m_bEnableUserActivity(this, "EnableUserActivity", true),
m_bEnableUserMood(this, "EnableUserMood", true),
m_bEnableUserTune(this, "EnableUserTune", false),
+ m_bEnableVOIP(this, "EnableVOIP", false),
m_bEnableZlib(this, "EnableZlib", true),
m_bFixIncorrectTimestamps(this, "FixIncorrectTimestamps", true),
m_bGcLogAffiliations(this, "GcLogAffiliations", false),
diff --git a/protocols/JabberG/src/jabber_proto.h b/protocols/JabberG/src/jabber_proto.h index ce8a4c0881..e13cd79e3e 100644 --- a/protocols/JabberG/src/jabber_proto.h +++ b/protocols/JabberG/src/jabber_proto.h @@ -203,6 +203,7 @@ struct CJabberProto : public PROTO<CJabberProto>, public IJabberInterface CMOption<bool> m_bEnableUserActivity;
CMOption<bool> m_bEnableUserMood;
CMOption<bool> m_bEnableUserTune;
+ CMOption<bool> m_bEnableVOIP;
CMOption<bool> m_bEnableZlib;
CMOption<bool> m_bFixIncorrectTimestamps;
CMOption<bool> m_bGcLogAffiliations;
@@ -886,6 +887,21 @@ struct CJabberProto : public PROTO<CJabberProto>, public IJabberInterface void SetServerVcard(BOOL bPhotoChanged, wchar_t* szPhotoFileName);
void SaveVcardToDB(HWND hwndPage, int iPage);
+ //---- jabber_voip.c -----------------------------------------------------------------
+
+ bool OnICECandidate(const TiXmlElement *Node, const char *from);
+ bool OnRTPDescription(const TiXmlElement *Node);
+ bool VOIPCreatePipeline();
+ bool VOIPTerminateSession();
+ bool VOIPCallAccept(const TiXmlElement *jingleNode, const char *from);
+ bool VOIPCallIinitiate(MCONTACT hContact);
+
+ CMStringA m_voipSession, m_voipPeerJid;
+ CMStringA m_voipICEPwd, m_voipICEUfrag, m_medianame;
+ bool m_isOutgoing;
+ struct _GstElement *m_pipe1 = NULL;
+ struct _GstElement *m_webrtc1 = NULL;
+
//---- jabber_xml.c ------------------------------------------------------------------
void OnConsoleProcessXml(const TiXmlElement *node, uint32_t flags);
diff --git a/protocols/JabberG/src/jabber_thread.cpp b/protocols/JabberG/src/jabber_thread.cpp index cedb5ed81b..ce3c613013 100644 --- a/protocols/JabberG/src/jabber_thread.cpp +++ b/protocols/JabberG/src/jabber_thread.cpp @@ -1770,41 +1770,78 @@ bool CJabberProto::OnProcessJingle(const TiXmlElement *node) if (child) {
if ((type = XmlGetAttr(node, "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 *szAction = XmlGetAttr(child, "action");
const char *idStr = XmlGetAttr(node, "id");
const char *from = XmlGetAttr(node, "from");
- if (szAction && !mir_strcmp(szAction, "session-initiate")) {
- // if this is a Jingle 'session-initiate' and noone processed it yet, reply with "unsupported-applications"
- m_ThreadInfo->send(XmlNodeIq("result", idStr, from));
-
- XmlNodeIq iq("set", SerialNext(), from);
- TiXmlElement *jingleNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
-
- jingleNode << XATTR("action", "session-terminate");
- const char *szInitiator = XmlGetAttr(child, "initiator");
- if (szInitiator)
- jingleNode << XATTR("initiator", szInitiator);
- const char *szSid = XmlGetAttr(child, "sid");
- if (szSid)
- jingleNode << XATTR("sid", szSid);
-
- jingleNode << XCHILD("reason")
- << XCHILD("unsupported-applications");
- m_ThreadInfo->send(iq);
- return true;
- }
- else {
- // 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");
- m_ThreadInfo->send(iq);
- return true;
+ 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"
+ m_ThreadInfo->send(XmlNodeIq("result", idStr, from));
+
+ const TiXmlElement *descr = XmlGetChildByTag(content, "description", "xmlns", JABBER_FEAT_JINGLE_RTP);
+ if (m_bEnableVOIP && m_voipSession == "" && descr) {
+ if (VOIPCallAccept(child, from)) {
+ m_voipSession = szSid;
+ m_voipPeerJid = from;
+ return true;
+ }
+ }
+
+ XmlNodeIq iq("set", SerialNext(), from);
+ TiXmlElement *jingleNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE);
+ jingleNode << XATTR("action", "session-terminate");
+ if (szInitiator)
+ jingleNode << XATTR("initiator", szInitiator);
+ if (szSid)
+ jingleNode << XATTR("sid", szSid);
+ jingleNode << XCHILD("reason") << XCHILD("unsupported-applications");
+
+ m_ThreadInfo->send(iq);
+ return true;
+ }
+ else if (!mir_strcmp(szAction, "session-accept")) {
+ if (m_bEnableVOIP && m_voipSession == szSid) {
+ m_ThreadInfo->send(XmlNodeIq("result", idStr, from));
+ OnRTPDescription(child);
+ return true;
+ }
+ }
+ else if (!mir_strcmp(szAction, "session-terminate")) {
+ if (m_bEnableVOIP && m_voipSession == szSid) {
+ // EndCall()
+ m_ThreadInfo->send(XmlNodeIq("result", idStr, from));
+ VOIPTerminateSession();
+ m_voipSession.Empty();
+ m_voipPeerJid.Empty();
+ return true;
+ }
+ }
+ else if (!mir_strcmp(szAction, "transport-info")) {
+ auto *transport = XmlGetChildByTag(content, "transport", "xmlns", JABBER_FEAT_JINGLE_ICEUDP);
+ if (m_bEnableVOIP && m_voipSession == szSid && transport) {
+ m_ThreadInfo->send(XmlNodeIq("result", idStr, from));
+ if (const TiXmlElement *candidate = XmlFirstChild(transport, "candidate")) {
+ OnICECandidate(candidate, from);
+ 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");
+ m_ThreadInfo->send(iq);
+ return true;
}
}
return false;
diff --git a/protocols/JabberG/src/jabber_voip.cpp b/protocols/JabberG/src/jabber_voip.cpp new file mode 100644 index 0000000000..2f45d61350 --- /dev/null +++ b/protocols/JabberG/src/jabber_voip.cpp @@ -0,0 +1,519 @@ +#include "stdafx.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> + +#pragma comment(lib, "glib-2.0.lib") +#pragma comment(lib, "gobject-2.0.lib") +#pragma comment(lib, "gstreamer-1.0.lib") +#pragma comment(lib, "gstrtp-1.0.lib") +#pragma comment(lib, "gstsdp-1.0.lib") +#pragma comment(lib, "gstwebrtc-1.0.lib") + +static std::list<CMStringA> remotecands; + +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; + CJabberProto *jproto = (CJabberProto *)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->SerialNext(), 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_ThreadInfo->fullJID : jproto->m_voipPeerJid); + if (!outgoing) + rjNode << XATTR("responder", jproto->m_ThreadInfo->fullJID); + + 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_ThreadInfo->send(iq); + + gst_webrtc_session_description_free(offer); +} + +void on_negotiation_needed_cb(GstElement *webrtcbin, gpointer user_data) +{ + if (((CJabberProto *)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(((CJabberProto *)user_data)->m_webrtc1, "create-answer", NULL, promise); +} + +void send_ice_candidate_message_cb(G_GNUC_UNUSED GstElement */*webrtcbin*/, guint mline_index, gchar *candidate, CJabberProto *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->SerialNext(), 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_ThreadInfo->send(iq); +} + +static gboolean check_plugins(void) +{ + const gchar *needed[] = { "opus", "nice", "webrtc", "dtls", "srtp", "rtpmanager", + /*"vpx", "videotestsrc", "audiotestsrc",*/ NULL }; + + GstRegistry *registry = gst_registry_get(); + gst_registry_scan_path(registry, "libs\\gst_plugins"); + gboolean ret = TRUE; + for (int i = 0; needed[i]; i++) { + GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); + if (!plugin) { + gst_print("Required gstreamer plugin '%s' not found\n", needed[i]); + ret = FALSE; + } + else gst_object_unref(plugin); + } + + return ret; +} + +void dbgprint(const gchar *string) +{ + OutputDebugStringA(string); +} + +bool CJabberProto::VOIPCreatePipeline(void) +{ + //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); + return false; + } + 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); + return false; + } + 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) + MessageBoxA(0, "Epic fail", "cannot create m_webrtc1", MB_OK); + + 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) + goto err; + + return true; + +err: + VOIPTerminateSession(); + return FALSE; +} + +bool CJabberProto::VOIPTerminateSession() +{ + gst_print("Terminating session"); + if (m_pipe1) { + gst_element_set_state(GST_ELEMENT(m_pipe1), GST_STATE_NULL); + g_clear_object(&m_pipe1); + gst_print("Pipeline stopped\n"); + gst_object_unref(m_pipe1); + } + + m_voipICEPwd.Empty(); + m_voipICEUfrag.Empty(); + m_medianame.Empty(); + + m_voipSession.Empty(); + m_voipPeerJid.Empty(); + m_pipe1 = m_webrtc1 = NULL; + return true; +} + +bool CJabberProto::OnRTPDescription(const TiXmlElement *jingleNode) +{ + // 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 CJabberProto::OnICECandidate(const TiXmlElement *Node, const char *) +{ + 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 CJabberProto::VOIPCallIinitiate(MCONTACT hContact) +{ + if (m_voipSession != "") { + VOIPTerminateSession(); + MessageBoxA(0, "Terminated", NULL, 0); + return 0; + } + + CMStringA jid(ptrA(getUStringA(hContact, "jid"))); + if (jid == "") + return 0; + ptrA szResource(GetBestResourceName(jid)); + if (szResource) + jid = MakeJid(jid, szResource); + + CMStringA question(FORMAT, "Call %s?\r\n" + "It will disclose IP address to the peer and his server", jid.c_str()); + if (MessageBoxA(0, question.c_str(), "Outgoing call", MB_YESNO | MB_ICONQUESTION) != IDYES) + return 0; + + 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(); + + VOIPCreatePipeline(); + return 0; +} + +bool CJabberProto::VOIPCallAccept(const TiXmlElement *jingleNode, const char *from) +{ + if (!from || !jingleNode) + return false; + + CMStringW question(FORMAT, TranslateT("Accept call from %s?\r\nIt will disclose IP address to the peer and his server"), from); + if (MessageBoxW(0, question, TranslateT("Incomig call"), MB_YESNO | MB_ICONQUESTION) != IDYES) + return false; + + m_isOutgoing = false; + + if (!VOIPCreatePipeline()) + return false; + + OnRTPDescription(jingleNode); + + // ringing message + XmlNodeIq iq("set", SerialNext(), from); + TiXmlElement *rjNode = iq << XCHILDNS("jingle", JABBER_FEAT_JINGLE); + rjNode << XATTR("action", "session-info"); + const char *szInitiator = XmlGetAttr(jingleNode, "initiator"); + if (szInitiator) + rjNode << XATTR("initiator", szInitiator); + const char *szSid = XmlGetAttr(jingleNode, "sid"); + if (szSid) + rjNode << XATTR("sid", szSid); + rjNode << XCHILDNS("ringing", "urn:xmpp:jingle:apps:rtp:info:1"); + + m_ThreadInfo->send(iq); + return true; +} |