|
|
@@ -1,4 +1,4 @@
|
|
1
|
|
-/* global $, __filename */
|
|
|
1
|
+/* global $, $build, __filename */
|
|
2
|
2
|
|
|
3
|
3
|
import { getLogger } from 'jitsi-meet-logger';
|
|
4
|
4
|
import { $iq, Strophe } from 'strophe.js';
|
|
|
@@ -22,6 +22,174 @@ const logger = getLogger(__filename);
|
|
22
|
22
|
// function call chains.
|
|
23
|
23
|
/* eslint-disable newline-per-chained-call */
|
|
24
|
24
|
|
|
|
25
|
+/**
|
|
|
26
|
+ * Reads a JSON-encoded message (from a "json-message" element) and extracts source descriptions. Adds the extracted
|
|
|
27
|
+ * source descriptions to the given Jingle IQ in the standard Jingle format.
|
|
|
28
|
+ *
|
|
|
29
|
+ * Encoding sources in this compact JSON format instead of standard Jingle was introduced in order to reduce the
|
|
|
30
|
+ * network traffic and load on the XMPP server. The format is described in Jicofo [TODO: insert link].
|
|
|
31
|
+ *
|
|
|
32
|
+ * @param {*} iq the IQ to which source descriptions will be added.
|
|
|
33
|
+ * @param {*} jsonMessageXml The XML node for the "json-message" element.
|
|
|
34
|
+ * @returns nothing.
|
|
|
35
|
+ */
|
|
|
36
|
+function expandSourcesFromJson(iq, jsonMessageXml) {
|
|
|
37
|
+
|
|
|
38
|
+ let json;
|
|
|
39
|
+
|
|
|
40
|
+ try {
|
|
|
41
|
+ json = JSON.parse(jsonMessageXml.textContent);
|
|
|
42
|
+ } catch (error) {
|
|
|
43
|
+ logger.error(`json-message XML contained invalid JSON, ignoring: ${jsonMessageXml.textContent}`);
|
|
|
44
|
+
|
|
|
45
|
+ return;
|
|
|
46
|
+ }
|
|
|
47
|
+
|
|
|
48
|
+ if (!json || !json.sources) {
|
|
|
49
|
+ // It might be a message of a different type, no need to log.
|
|
|
50
|
+ return;
|
|
|
51
|
+ }
|
|
|
52
|
+
|
|
|
53
|
+ // This is where we'll add "source" and "ssrc-group" elements. Create them elements if they don't exist.
|
|
|
54
|
+ const videoRtpDescription = getOrCreateRtpDescription(iq, 'video');
|
|
|
55
|
+ const audioRtpDescription = getOrCreateRtpDescription(iq, 'audio');
|
|
|
56
|
+
|
|
|
57
|
+ for (const owner in json.sources) {
|
|
|
58
|
+ if (json.sources.hasOwnProperty(owner)) {
|
|
|
59
|
+
|
|
|
60
|
+ const ownerSources = json.sources[owner];
|
|
|
61
|
+
|
|
|
62
|
+ // The video sources, video ssrc-groups, audio sources and audio ssrc-groups are encoded in that order in
|
|
|
63
|
+ // the elements of the array.
|
|
|
64
|
+ const videoSources = ownerSources && ownerSources.length > 0 && ownerSources[0];
|
|
|
65
|
+ const videoSsrcGroups = ownerSources && ownerSources.length > 1 && ownerSources[1];
|
|
|
66
|
+ const audioSources = ownerSources && ownerSources.length > 2 && ownerSources[2];
|
|
|
67
|
+ const audioSsrcGroups = ownerSources && ownerSources.length > 3 && ownerSources[3];
|
|
|
68
|
+
|
|
|
69
|
+ if (videoSources && videoSources.length > 0) {
|
|
|
70
|
+ for (let i = 0; i < videoSources.length; i++) {
|
|
|
71
|
+ videoRtpDescription.appendChild(createSourceExtension(owner, videoSources[i]));
|
|
|
72
|
+ }
|
|
|
73
|
+ }
|
|
|
74
|
+ if (videoSsrcGroups && videoSsrcGroups.length > 0) {
|
|
|
75
|
+ for (let i = 0; i < videoSsrcGroups.length; i++) {
|
|
|
76
|
+ videoRtpDescription.appendChild(createSsrcGroupExtension(videoSsrcGroups[i]));
|
|
|
77
|
+ }
|
|
|
78
|
+ }
|
|
|
79
|
+ if (audioSources && audioSources.length > 0) {
|
|
|
80
|
+ for (let i = 0; i < audioSources.length; i++) {
|
|
|
81
|
+ audioRtpDescription.appendChild(createSourceExtension(owner, audioSources[i]));
|
|
|
82
|
+ }
|
|
|
83
|
+ }
|
|
|
84
|
+ if (audioSsrcGroups && audioSsrcGroups.length > 0) {
|
|
|
85
|
+ for (let i = 0; i < audioSsrcGroups.length; i++) {
|
|
|
86
|
+ audioRtpDescription.appendChild(createSsrcGroupExtension(audioSsrcGroups[i]));
|
|
|
87
|
+ }
|
|
|
88
|
+ }
|
|
|
89
|
+ }
|
|
|
90
|
+ }
|
|
|
91
|
+}
|
|
|
92
|
+
|
|
|
93
|
+/**
|
|
|
94
|
+ * Creates a "source" XML element for the source described in compact JSON format in [sourceCompactJson].
|
|
|
95
|
+ * @param {*} owner the endpoint ID of the owner of the source.
|
|
|
96
|
+ * @param {*} sourceCompactJson the compact JSON representation of the source.
|
|
|
97
|
+ * @returns the created "source" XML element.
|
|
|
98
|
+ */
|
|
|
99
|
+function createSourceExtension(owner, sourceCompactJson) {
|
|
|
100
|
+ const node = $build('source', {
|
|
|
101
|
+ xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
|
|
|
102
|
+ ssrc: sourceCompactJson.s
|
|
|
103
|
+ });
|
|
|
104
|
+
|
|
|
105
|
+ if (sourceCompactJson.m) {
|
|
|
106
|
+ node.c('parameter', {
|
|
|
107
|
+ name: 'msid',
|
|
|
108
|
+ value: sourceCompactJson.m
|
|
|
109
|
+ }).up();
|
|
|
110
|
+ }
|
|
|
111
|
+ node.c('ssrc-info', {
|
|
|
112
|
+ xmlns: 'http://jitsi.org/jitmeet',
|
|
|
113
|
+ owner
|
|
|
114
|
+ }).up();
|
|
|
115
|
+
|
|
|
116
|
+ return node.node;
|
|
|
117
|
+}
|
|
|
118
|
+
|
|
|
119
|
+/**
|
|
|
120
|
+ * Creates an "ssrc-group" XML element for the SSRC group described in compact JSON format in [ssrcGroupCompactJson].
|
|
|
121
|
+ * @param {*} ssrcGroupCompactJson the compact JSON representation of the SSRC group.
|
|
|
122
|
+ * @returns the created "ssrc-group" element.
|
|
|
123
|
+ */
|
|
|
124
|
+function createSsrcGroupExtension(ssrcGroupCompactJson) {
|
|
|
125
|
+ const node = $build('ssrc-group', {
|
|
|
126
|
+ xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
|
|
|
127
|
+ semantics: getSemantics(ssrcGroupCompactJson[0])
|
|
|
128
|
+ });
|
|
|
129
|
+
|
|
|
130
|
+ for (let i = 1; i < ssrcGroupCompactJson.length; i++) {
|
|
|
131
|
+ node.c('source', {
|
|
|
132
|
+ xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0',
|
|
|
133
|
+ ssrc: ssrcGroupCompactJson[i]
|
|
|
134
|
+ }).up();
|
|
|
135
|
+ }
|
|
|
136
|
+
|
|
|
137
|
+ return node.node;
|
|
|
138
|
+}
|
|
|
139
|
+
|
|
|
140
|
+/**
|
|
|
141
|
+ * Convert the short string representing SSRC group semantics in compact JSON format to the standard representation
|
|
|
142
|
+ * (i.e. convert "f" to "FID" and "s" to "SIM").
|
|
|
143
|
+ * @param {*} str the compact JSON format representation of an SSRC group's semantics.
|
|
|
144
|
+ * @returns the SSRC group semantics corresponding to [str].
|
|
|
145
|
+ */
|
|
|
146
|
+function getSemantics(str) {
|
|
|
147
|
+ if (str === 'f') {
|
|
|
148
|
+ return 'FID';
|
|
|
149
|
+ } else if (str === 's') {
|
|
|
150
|
+ return 'SIM';
|
|
|
151
|
+ }
|
|
|
152
|
+
|
|
|
153
|
+ return null;
|
|
|
154
|
+}
|
|
|
155
|
+
|
|
|
156
|
+/**
|
|
|
157
|
+ * Finds in a Jingle IQ the RTP description element with the given media type. If one does not exists, create it (as
|
|
|
158
|
+ * well as the required "content" parent element) and adds it to the IQ.
|
|
|
159
|
+ * @param {*} iq
|
|
|
160
|
+ * @param {*} mediaType The media type, "audio" or "video".
|
|
|
161
|
+ * @returns the RTP description element with the given media type.
|
|
|
162
|
+ */
|
|
|
163
|
+function getOrCreateRtpDescription(iq, mediaType) {
|
|
|
164
|
+ const jingle = $(iq).find('jingle')[0];
|
|
|
165
|
+ let content = $(jingle).find(`content[name="${mediaType}"]`);
|
|
|
166
|
+ let description;
|
|
|
167
|
+
|
|
|
168
|
+ if (content.length) {
|
|
|
169
|
+ content = content[0];
|
|
|
170
|
+ } else {
|
|
|
171
|
+ // I'm not suree if "creator" and "senders" are required.
|
|
|
172
|
+ content = $build('content', {
|
|
|
173
|
+ name: mediaType
|
|
|
174
|
+ }).node;
|
|
|
175
|
+ jingle.appendChild(content);
|
|
|
176
|
+ }
|
|
|
177
|
+
|
|
|
178
|
+ description = $(content).find('description');
|
|
|
179
|
+
|
|
|
180
|
+ if (description.length) {
|
|
|
181
|
+ description = description[0];
|
|
|
182
|
+ } else {
|
|
|
183
|
+ description = $build('description', {
|
|
|
184
|
+ xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
|
|
185
|
+ media: mediaType
|
|
|
186
|
+ }).node;
|
|
|
187
|
+ content.appendChild(description);
|
|
|
188
|
+ }
|
|
|
189
|
+
|
|
|
190
|
+ return description;
|
|
|
191
|
+}
|
|
|
192
|
+
|
|
25
|
193
|
/**
|
|
26
|
194
|
*
|
|
27
|
195
|
*/
|
|
|
@@ -132,6 +300,18 @@ export default class JingleConnectionPlugin extends ConnectionPlugin {
|
|
132
|
300
|
|
|
133
|
301
|
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
|
|
134
|
302
|
|
|
|
303
|
+ const jsonMessages = $(iq).find('jingle>json-message');
|
|
|
304
|
+
|
|
|
305
|
+ if (jsonMessages && jsonMessages.length > 0) {
|
|
|
306
|
+ logger.info(`Found a JSON-encoded element in ${action}, translating to standard Jingle.`);
|
|
|
307
|
+ for (let i = 0; i < jsonMessages.length; i++) {
|
|
|
308
|
+ expandSourcesFromJson(iq, jsonMessages[i]);
|
|
|
309
|
+ }
|
|
|
310
|
+
|
|
|
311
|
+ // TODO: is there a way to remove the json-message elements once we've extracted the information?
|
|
|
312
|
+ // removeChild doesn't seem to work.
|
|
|
313
|
+ }
|
|
|
314
|
+
|
|
135
|
315
|
switch (action) {
|
|
136
|
316
|
case 'session-initiate': {
|
|
137
|
317
|
logger.log('(TIME) received session-initiate:\t', now);
|