@reactoo/watchtogether-sdk-js 2.7.97 → 2.8.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +2 -1
- package/dist/watchtogether-sdk.js +57376 -0
- package/dist/watchtogether-sdk.js.map +1 -0
- package/dist/watchtogether-sdk.min.js +2 -2
- package/example/index.html +69 -32
- package/package.json +2 -1
- package/src/index.js +1 -1
- package/src/models/asset.js +2 -2
- package/src/models/room-session.js +14 -14
- package/src/modules/wt-iot2.js +9 -9
- package/src/modules/wt-room-sfu.js +3692 -0
- package/src/modules/wt-room.js +14 -5
|
@@ -0,0 +1,3692 @@
|
|
|
1
|
+
// Watch together janus webrtc library
|
|
2
|
+
|
|
3
|
+
import adapter from 'webrtc-adapter';
|
|
4
|
+
import emitter from './wt-emitter';
|
|
5
|
+
import {decodeJanusDisplay, generateUUID, maxJitter, median, wait} from "../models/utils";
|
|
6
|
+
|
|
7
|
+
class Room {
|
|
8
|
+
|
|
9
|
+
static noop() {
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
constructor(debug) {
|
|
14
|
+
this.debug = debug;
|
|
15
|
+
this.sessions = [];
|
|
16
|
+
this.safariVp8 = false;
|
|
17
|
+
this.browser = adapter.browserDetails.browser;
|
|
18
|
+
this.browserDetails = adapter.browserDetails;
|
|
19
|
+
this.webrtcSupported = Room.isWebrtcSupported();
|
|
20
|
+
this.safariVp8TestPromise = Room.testSafariVp8();
|
|
21
|
+
this.safariVp8 = null;
|
|
22
|
+
this.safariVp8TestPromise.then(safariVp8 => {
|
|
23
|
+
this.safariVp8 = safariVp8;
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
this._log = Room.noop;
|
|
27
|
+
if (this.debug) {
|
|
28
|
+
this.#enableDebug();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Let's get it started
|
|
32
|
+
this.whenInitialized = this.initialize();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#enableDebug() {
|
|
36
|
+
this._log = console.log.bind(console);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
initialize() {
|
|
40
|
+
return this.safariVp8TestPromise
|
|
41
|
+
.then(() => this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createSession(constructId = null, type = 'reactooroom', options = {}) {
|
|
45
|
+
return new RoomSession(constructId, type, {debug: this.debug, ...options});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static testSafariVp8() {
|
|
49
|
+
return new Promise(resolve => {
|
|
50
|
+
if (adapter.browserDetails.browser === 'safari' &&
|
|
51
|
+
adapter.browserDetails.version >= 605) {
|
|
52
|
+
if (RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") &&
|
|
53
|
+
RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) {
|
|
54
|
+
var isVp8 = false;
|
|
55
|
+
for (var i in RTCRtpSender.getCapabilities("video").codecs) {
|
|
56
|
+
var codec = RTCRtpSender.getCapabilities("video").codecs[i];
|
|
57
|
+
if (codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") {
|
|
58
|
+
isVp8 = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
resolve(isVp8);
|
|
63
|
+
} else {
|
|
64
|
+
// We do it in a very ugly way, as there's no alternative...
|
|
65
|
+
// We create a PeerConnection to see if VP8 is in an offer
|
|
66
|
+
var testpc = new RTCPeerConnection({}, {});
|
|
67
|
+
testpc.createOffer({offerToReceiveVideo: true}).then(function (offer) {
|
|
68
|
+
let result = offer.sdp.indexOf("VP8") !== -1;
|
|
69
|
+
testpc.close();
|
|
70
|
+
testpc = null;
|
|
71
|
+
resolve(result);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} else resolve(false);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static isWebrtcSupported() {
|
|
79
|
+
return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null &&
|
|
80
|
+
navigator.mediaDevices !== undefined && navigator.mediaDevices !== null &&
|
|
81
|
+
navigator.mediaDevices.getUserMedia !== undefined && navigator.mediaDevices.getUserMedia !== null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class RoomSession {
|
|
86
|
+
|
|
87
|
+
static noop() {
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static randomString(len) {
|
|
92
|
+
var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
93
|
+
var randomString = '';
|
|
94
|
+
for (var i = 0; i < len; i++) {
|
|
95
|
+
var randomPoz = Math.floor(Math.random() * charSet.length);
|
|
96
|
+
randomString += charSet.substring(randomPoz, randomPoz + 1);
|
|
97
|
+
}
|
|
98
|
+
return randomString;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static sessionTypes = {
|
|
102
|
+
'reactooroom': 'janus.plugin.reactooroom',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static userRoleSubscriptionRules = {
|
|
106
|
+
participant: {
|
|
107
|
+
"watchparty": ['participant', 'talkback'],
|
|
108
|
+
"studio": ['participant', 'talkback', 'host', 'observer'],
|
|
109
|
+
"commentary": ['participant', 'talkback', 'host'],
|
|
110
|
+
"intercom": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
|
|
111
|
+
"videowall": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
|
|
112
|
+
"videowall-queue": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
|
|
113
|
+
"videowall-queue-video": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5']
|
|
114
|
+
},
|
|
115
|
+
monitor: {
|
|
116
|
+
"watchparty": ['participant', 'host'],
|
|
117
|
+
"studio": ['participant', 'host', 'observer'],
|
|
118
|
+
"commentary": ['participant', 'host'],
|
|
119
|
+
"intercom": ['host', 'participant'],
|
|
120
|
+
"videowall": ['host', 'participant'],
|
|
121
|
+
"videowall-queue": ['host', 'participant'],
|
|
122
|
+
"videowall-queue-video": ['host', 'participant'],
|
|
123
|
+
},
|
|
124
|
+
talkback: {
|
|
125
|
+
"watchparty": ['participant', 'host', 'talkback'],
|
|
126
|
+
"studio": ['participant', 'host', 'observer', 'talkback'],
|
|
127
|
+
"commentary": ['host', 'participant', 'talkback'],
|
|
128
|
+
"intercom": ['host', 'participant', 'talkback'],
|
|
129
|
+
"videowall": ['host', 'participant', 'talkback'],
|
|
130
|
+
"videowall-queue": ['host', 'participant', 'talkback'],
|
|
131
|
+
"videowall-queue-video": ['host', 'participant', 'talkback'],
|
|
132
|
+
},
|
|
133
|
+
observer: {
|
|
134
|
+
"watchparty": ['participant'],
|
|
135
|
+
"studio": ['participant'],
|
|
136
|
+
"commentary": ['participant'],
|
|
137
|
+
"intercom": ['participant'],
|
|
138
|
+
"videowall": ['participant'],
|
|
139
|
+
"videowall-queue": ['participant'],
|
|
140
|
+
"videowall-queue-video": ['participant'],
|
|
141
|
+
},
|
|
142
|
+
observerSolo1: {
|
|
143
|
+
"watchparty": ['participant'],
|
|
144
|
+
"studio": ['participant'],
|
|
145
|
+
"commentary": ['participant'],
|
|
146
|
+
"intercom": ['participant'],
|
|
147
|
+
"videowall": ['participant'],
|
|
148
|
+
"videowall-queue": ['participant'],
|
|
149
|
+
"videowall-queue-video": ['participant'],
|
|
150
|
+
},
|
|
151
|
+
observerSolo2: {
|
|
152
|
+
"watchparty": ['participant'],
|
|
153
|
+
"studio": ['participant'],
|
|
154
|
+
"commentary": ['participant'],
|
|
155
|
+
"intercom": ['participant'],
|
|
156
|
+
"videowall": ['participant'],
|
|
157
|
+
"videowall-queue": ['participant'],
|
|
158
|
+
"videowall-queue-video": ['participant'],
|
|
159
|
+
},
|
|
160
|
+
observerSolo3: {
|
|
161
|
+
"watchparty": ['participant'],
|
|
162
|
+
"studio": ['participant'],
|
|
163
|
+
"commentary": ['participant'],
|
|
164
|
+
"intercom": ['participant'],
|
|
165
|
+
"videowall": ['participant'],
|
|
166
|
+
"videowall-queue": ['participant'],
|
|
167
|
+
"videowall-queue-video": ['participant'],
|
|
168
|
+
},
|
|
169
|
+
observerSolo4: {
|
|
170
|
+
"watchparty": ['participant'],
|
|
171
|
+
"studio": ['participant'],
|
|
172
|
+
"commentary": ['participant'],
|
|
173
|
+
"intercom": ['participant'],
|
|
174
|
+
"videowall": ['participant'],
|
|
175
|
+
"videowall-queue": ['participant'],
|
|
176
|
+
"videowall-queue-video": ['participant'],
|
|
177
|
+
},
|
|
178
|
+
observerSolo5: {
|
|
179
|
+
"watchparty": ['participant'],
|
|
180
|
+
"studio": ['participant'],
|
|
181
|
+
"commentary": ['participant'],
|
|
182
|
+
"intercom": ['participant'],
|
|
183
|
+
"videowall": ['participant'],
|
|
184
|
+
"videowall-queue": ['participant'],
|
|
185
|
+
"videowall-queue-video": ['participant'],
|
|
186
|
+
},
|
|
187
|
+
host: {
|
|
188
|
+
"watchparty": [],
|
|
189
|
+
"studio": [],
|
|
190
|
+
"commentary": [],
|
|
191
|
+
"intercom": [],
|
|
192
|
+
"videowall": [],
|
|
193
|
+
"videowall-queue": [],
|
|
194
|
+
"videowall-queue-video": [],
|
|
195
|
+
},
|
|
196
|
+
companionTV: {},
|
|
197
|
+
companionPhone: {},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
#publisherHandle = null;
|
|
201
|
+
#subscriberHandle = null;
|
|
202
|
+
#subscriberJoinPromise = null;
|
|
203
|
+
|
|
204
|
+
// Configuration for simulcast quality selection
|
|
205
|
+
#rtcStatsConfig = {
|
|
206
|
+
// Network thresholds
|
|
207
|
+
packetLossThreshold: 3.0, // Percentage of packet loss to trigger downgrade
|
|
208
|
+
jitterBufferThreshold: 100, // ms of jitter buffer delay to trigger downgrade
|
|
209
|
+
roundTripTimeThreshold: 250, // ms of RTT to trigger downgrade
|
|
210
|
+
frameDropRateThreshold: 5, // Percentage of frame drops to trigger downgrade
|
|
211
|
+
freezeLengthThreshold: 0.7, // 70% of freeze time for current monitored period
|
|
212
|
+
// Hysteresis to prevent rapid switching (ms)
|
|
213
|
+
switchCooldown: 15000, // Min time between switches
|
|
214
|
+
switchCooldownForFreeze: 3000,
|
|
215
|
+
historySize: 60000, // Ms of history to keep
|
|
216
|
+
badBandwidthThresholdMultiplier: 0.8, // 80% of required bitrate
|
|
217
|
+
// Stability thresholds
|
|
218
|
+
stableNetworkTime: 10000, // Time in ms to consider network stable
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
constructor(constructId = null, type = 'reactooroom', options = {}) {
|
|
223
|
+
|
|
224
|
+
Object.assign(this, emitter());
|
|
225
|
+
this.options = {...options};
|
|
226
|
+
this.defaultDataChannelLabel = 'JanusDataChannel'
|
|
227
|
+
this.server = null;
|
|
228
|
+
this.iceServers = null;
|
|
229
|
+
this.token = null;
|
|
230
|
+
this.roomId = null;
|
|
231
|
+
this.pin = null;
|
|
232
|
+
this.userId = null;
|
|
233
|
+
this.initialBitrate = 0;
|
|
234
|
+
this.enableDtx = false;
|
|
235
|
+
this.simulcast = false;
|
|
236
|
+
|
|
237
|
+
this.defaultSimulcastSettings = {
|
|
238
|
+
"default" : {
|
|
239
|
+
mode: "controlled", // controlled, manual, browserControlled
|
|
240
|
+
defaultSubstream: 0, // 2 lowest quality, 0 highest quality
|
|
241
|
+
bitrates: [
|
|
242
|
+
{
|
|
243
|
+
"rid": "l",
|
|
244
|
+
"active": true,
|
|
245
|
+
"maxBitrate": 180000,
|
|
246
|
+
"maxFramerate": 20,
|
|
247
|
+
"scaleResolutionDownBy": 3.3333333333333335,
|
|
248
|
+
"priority": "low"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"rid": "m",
|
|
252
|
+
"active": true,
|
|
253
|
+
"maxBitrate": 500000,
|
|
254
|
+
"maxFramerate": 25,
|
|
255
|
+
"scaleResolutionDownBy": 1.3333333333333335,
|
|
256
|
+
"priority": "low"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"rid": "h",
|
|
260
|
+
"active": true,
|
|
261
|
+
"maxBitrate": 2000000,
|
|
262
|
+
"maxFramerate": 30,
|
|
263
|
+
"priority": "low"
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
this.recordingFilename = null;
|
|
269
|
+
this.pluginName = RoomSession.sessionTypes[type];
|
|
270
|
+
this.id = null;
|
|
271
|
+
this.privateId = null;
|
|
272
|
+
this.handleId = null;
|
|
273
|
+
this.constructId = constructId || RoomSession.randomString(16);
|
|
274
|
+
this.sessionId = null;
|
|
275
|
+
this.apisecret = null;
|
|
276
|
+
this.ws = null;
|
|
277
|
+
|
|
278
|
+
// helper flags
|
|
279
|
+
|
|
280
|
+
this.isSupposeToBeConnected = false;
|
|
281
|
+
this.isConnecting = false;
|
|
282
|
+
this.isEstablishingConnection = false;
|
|
283
|
+
this.isDisconnecting = false;
|
|
284
|
+
this.isConnected = false;
|
|
285
|
+
this.isPublished = false;
|
|
286
|
+
|
|
287
|
+
this.isMuted = [];
|
|
288
|
+
|
|
289
|
+
this.requestMuteStatusTimeoutId = null;
|
|
290
|
+
this.requestMuteStatusTimeout = 100;
|
|
291
|
+
this._statsInterval = 3000;
|
|
292
|
+
this._statsIntervalId = null;
|
|
293
|
+
this._sendMessageTimeout = 10000;
|
|
294
|
+
this._keepAlivePeriod = 25000;
|
|
295
|
+
this._longPollTimeout = 60000;
|
|
296
|
+
this._maxev = 10;
|
|
297
|
+
this._keepAliveId = null;
|
|
298
|
+
this._restrictSubscribeToUserIds = []; // all if empty
|
|
299
|
+
this._talkIntercomChannels = ['participants'];
|
|
300
|
+
this._listenIntercomChannels = ['participants'];
|
|
301
|
+
this._roomType = 'watchparty';
|
|
302
|
+
this._isDataChannelOpen = false;
|
|
303
|
+
this._abortController = null;
|
|
304
|
+
|
|
305
|
+
this.userRoleSubscriptionRules = {
|
|
306
|
+
...RoomSession.userRoleSubscriptionRules,
|
|
307
|
+
...(this.options.userRoleSubscriptionRules || {})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this._log = RoomSession.noop;
|
|
311
|
+
if (this.options.debug) {
|
|
312
|
+
this.#enableDebug();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#httpAPICall = function(url, options) {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const xhr = new XMLHttpRequest();
|
|
320
|
+
|
|
321
|
+
xhr.open(options.verb || 'POST', url);
|
|
322
|
+
|
|
323
|
+
// Set default headers
|
|
324
|
+
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
|
|
325
|
+
if (options.verb === "POST") {
|
|
326
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Handle credentials
|
|
330
|
+
if (options.withCredentials) {
|
|
331
|
+
xhr.withCredentials = true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Set timeout if specified
|
|
335
|
+
if (options.timeout) {
|
|
336
|
+
xhr.timeout = options.timeout;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Setup handlers
|
|
340
|
+
xhr.onload = function() {
|
|
341
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
342
|
+
let response;
|
|
343
|
+
try {
|
|
344
|
+
response = JSON.parse(xhr.responseText);
|
|
345
|
+
} catch(e) {
|
|
346
|
+
if (options.error) {
|
|
347
|
+
options.error('Invalid JSON', xhr.responseText);
|
|
348
|
+
}
|
|
349
|
+
reject('Invalid JSON');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (options.success) {
|
|
353
|
+
options.success(response);
|
|
354
|
+
}
|
|
355
|
+
resolve(response);
|
|
356
|
+
} else {
|
|
357
|
+
let errorText = xhr.statusText || 'HTTP Error';
|
|
358
|
+
if (options.error) {
|
|
359
|
+
options.error(errorText, xhr.responseText);
|
|
360
|
+
}
|
|
361
|
+
reject(errorText);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
xhr.onerror = function() {
|
|
366
|
+
let errorText = 'Network error';
|
|
367
|
+
if (options.error) {
|
|
368
|
+
options.error(errorText, 'Is the server down?');
|
|
369
|
+
}
|
|
370
|
+
reject(errorText);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
xhr.ontimeout = function() {
|
|
374
|
+
let errorText = 'Request timed out';
|
|
375
|
+
if (options.error) {
|
|
376
|
+
options.error(errorText, 'Is the server down?');
|
|
377
|
+
}
|
|
378
|
+
reject(errorText);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Send request
|
|
382
|
+
try {
|
|
383
|
+
if (options.body) {
|
|
384
|
+
xhr.send(JSON.stringify(options.body));
|
|
385
|
+
} else {
|
|
386
|
+
xhr.send();
|
|
387
|
+
}
|
|
388
|
+
} catch(e) {
|
|
389
|
+
if (options.error) {
|
|
390
|
+
options.error('Error sending request', e);
|
|
391
|
+
}
|
|
392
|
+
reject(e);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#cacheAttendee(attendee) {
|
|
398
|
+
if(this.#subscriberHandle) {
|
|
399
|
+
this.#subscriberHandle.webrtcStuff.userIdToDisplay[attendee.id] = attendee.display;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
#removeAttendeeFromCache(id) {
|
|
404
|
+
if(this.#subscriberHandle) {
|
|
405
|
+
delete this.#subscriberHandle.webrtcStuff.userIdToDisplay[id];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#getCachedAttendeeIds() {
|
|
410
|
+
return Object.keys(this.#subscriberHandle?.webrtcStuff?.userIdToDisplay || {});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#getDisplayById(id) {
|
|
414
|
+
return this.#subscriberHandle.webrtcStuff.userIdToDisplay[id];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#emitLocalFeedUpdate({feedRemoval = false, addingTrack = false, removingTrack = false, track = null, source = null, mid = null} = {}) {
|
|
418
|
+
|
|
419
|
+
if(!this.id) {
|
|
420
|
+
// we're not connected probably at all
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const eventName = !feedRemoval ? 'addLocalParticipant' : 'removeLocalParticipant';
|
|
425
|
+
const streamMap = this.#getStreamMapForFeed(this.id);
|
|
426
|
+
|
|
427
|
+
const sourceTrackIds = streamMap[source] ?? [];
|
|
428
|
+
const sourceRelatedTracks = this.#publisherHandle?.webrtcStuff?.tracks?.filter(t => sourceTrackIds.includes(t.id)) ?? [];
|
|
429
|
+
|
|
430
|
+
const tracks = this.#publisherHandle?.webrtcStuff?.tracks ?? [];
|
|
431
|
+
|
|
432
|
+
this.emit(eventName, {
|
|
433
|
+
tid: generateUUID(),
|
|
434
|
+
id: this.id,
|
|
435
|
+
constructId: this.constructId,
|
|
436
|
+
userId: decodeJanusDisplay(this.userId)?.userId,
|
|
437
|
+
fullUserId: this.display,
|
|
438
|
+
role: decodeJanusDisplay(this.display)?.role,
|
|
439
|
+
track: track,
|
|
440
|
+
source: source,
|
|
441
|
+
adding: addingTrack,
|
|
442
|
+
removing: removingTrack,
|
|
443
|
+
streamMap: streamMap,
|
|
444
|
+
sourceRelatedTracks: sourceRelatedTracks,
|
|
445
|
+
tracks: tracks.map(t=>t),
|
|
446
|
+
hasAudioTrack: !!tracks.find(track => track.kind === 'audio'),
|
|
447
|
+
hasVideoTrack: !!tracks.find(track => track.kind === 'video'),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#emitRemoteFeedUpdate(mid, {feedRemoval = false, addingTrack = false, removingTrack = false, track = null, source = null, attendee = null} = {}) {
|
|
452
|
+
|
|
453
|
+
const transceiverData = this.#getTransceiverDataByMid(mid);
|
|
454
|
+
|
|
455
|
+
if(transceiverData) {
|
|
456
|
+
|
|
457
|
+
const id = transceiverData.feed_id;
|
|
458
|
+
const display = transceiverData.feed_display;
|
|
459
|
+
const parsedDisplay = decodeJanusDisplay(display);
|
|
460
|
+
let description = {};
|
|
461
|
+
try {
|
|
462
|
+
description = JSON.parse(transceiverData.feed_description);
|
|
463
|
+
} catch (e) {}
|
|
464
|
+
|
|
465
|
+
if(!this.#shouldEmitFeedUpdate(parsedDisplay)) {
|
|
466
|
+
this._log('Not emitting feed update for', display, 'because of subscription rules');
|
|
467
|
+
// we don't want to emit this event
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const source = description.source;
|
|
472
|
+
const role = parsedDisplay?.role;
|
|
473
|
+
const userId = parsedDisplay?.userId;
|
|
474
|
+
const eventName = this.#getParticipantEventName(!feedRemoval, role);
|
|
475
|
+
const streamMap = this.#getStreamMapForFeed(id);
|
|
476
|
+
|
|
477
|
+
const sourceTrackIds = streamMap[source] ?? [];
|
|
478
|
+
const sourceRelatedTracks = this.#subscriberHandle?.webrtcStuff?.tracks?.filter(t => sourceTrackIds.includes(t.id)) ?? [];
|
|
479
|
+
|
|
480
|
+
const trackIds = Object.keys(streamMap).reduce((acc, key) => {
|
|
481
|
+
streamMap[key].forEach(trackId => {
|
|
482
|
+
if (!acc.includes(trackId)) {
|
|
483
|
+
acc.push(trackId);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
return acc;
|
|
487
|
+
}, []);
|
|
488
|
+
|
|
489
|
+
const tracks = this.#subscriberHandle?.webrtcStuff?.tracks?.filter(t => trackIds.includes(t.id)) ?? [];
|
|
490
|
+
|
|
491
|
+
this.emit(eventName, {
|
|
492
|
+
tid: generateUUID(),
|
|
493
|
+
id,
|
|
494
|
+
userId,
|
|
495
|
+
role,
|
|
496
|
+
fullUserId: display,
|
|
497
|
+
constructId: this.constructId,
|
|
498
|
+
track: track,
|
|
499
|
+
source: source,
|
|
500
|
+
adding: addingTrack,
|
|
501
|
+
removing: removingTrack,
|
|
502
|
+
streamMap,
|
|
503
|
+
sourceRelatedTracks,
|
|
504
|
+
tracks,
|
|
505
|
+
hasAudioTrack: !!tracks.find(track => track.kind === 'audio'),
|
|
506
|
+
hasVideoTrack: !!tracks.find(track => track.kind === 'video'),
|
|
507
|
+
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
} else if(attendee) {
|
|
511
|
+
|
|
512
|
+
const id = attendee.id;
|
|
513
|
+
const display = this.#getDisplayById(id);
|
|
514
|
+
const parsedDisplay = decodeJanusDisplay(display);
|
|
515
|
+
|
|
516
|
+
if(!this.#shouldEmitFeedUpdate(parsedDisplay)) {
|
|
517
|
+
this._log('Not emitting feed update for', display, 'because of subscription rules');
|
|
518
|
+
// we don't want to emit this event
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const role = parsedDisplay?.role;
|
|
523
|
+
const userId = parsedDisplay?.userId;
|
|
524
|
+
const streamMap = this.#getStreamMapForFeed(id);
|
|
525
|
+
const eventName = this.#getParticipantEventName(!feedRemoval, role);
|
|
526
|
+
|
|
527
|
+
this.emit(eventName, {
|
|
528
|
+
tid: generateUUID(),
|
|
529
|
+
id,
|
|
530
|
+
userId,
|
|
531
|
+
role,
|
|
532
|
+
fullUserId: display,
|
|
533
|
+
constructId: this.constructId,
|
|
534
|
+
track: null,
|
|
535
|
+
source: null,
|
|
536
|
+
adding: false,
|
|
537
|
+
removing: false,
|
|
538
|
+
streamMap,
|
|
539
|
+
sourceRelatedTracks: [],
|
|
540
|
+
tracks: [],
|
|
541
|
+
hasAudioTrack: false,
|
|
542
|
+
hasVideoTrack: false,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ask for current mute status to each participant
|
|
547
|
+
|
|
548
|
+
clearTimeout(this.requestMuteStatusTimeoutId);
|
|
549
|
+
this.requestMuteStatusTimeoutId = setTimeout(() => {
|
|
550
|
+
if(this.isConnected) {
|
|
551
|
+
this.emit('requestMuteStatus')
|
|
552
|
+
}
|
|
553
|
+
}, this.requestMuteStatusTimeout)
|
|
554
|
+
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#emitRemoteTrackMuted(mid, {track, muted} = {}) {
|
|
558
|
+
|
|
559
|
+
const transceiverData = this.#getTransceiverDataByMid(mid);
|
|
560
|
+
|
|
561
|
+
if(transceiverData) {
|
|
562
|
+
|
|
563
|
+
const id = transceiverData.feed_id;
|
|
564
|
+
const display = transceiverData.feed_display;
|
|
565
|
+
const parsedDisplay = decodeJanusDisplay(display);
|
|
566
|
+
let description = {};
|
|
567
|
+
try {
|
|
568
|
+
description = JSON.parse(transceiverData.feed_description);
|
|
569
|
+
} catch (e) {}
|
|
570
|
+
|
|
571
|
+
const source = description.source;
|
|
572
|
+
const role = parsedDisplay?.role;
|
|
573
|
+
const userId = parsedDisplay?.userId;
|
|
574
|
+
const streamMap = this.#getStreamMapForFeed(id);
|
|
575
|
+
|
|
576
|
+
this.emit('remoteTrackMuted', {
|
|
577
|
+
tid: generateUUID(),
|
|
578
|
+
id,
|
|
579
|
+
userId,
|
|
580
|
+
role,
|
|
581
|
+
mid,
|
|
582
|
+
fullUserId: display,
|
|
583
|
+
constructId: this.constructId,
|
|
584
|
+
streamMap,
|
|
585
|
+
source,
|
|
586
|
+
kind: track.kind,
|
|
587
|
+
track:track,
|
|
588
|
+
muted
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#getStreamMapForFeed(id) {
|
|
594
|
+
if (id === this.id) {
|
|
595
|
+
return this.#publisherHandle?.webrtcStuff?.streamMap?.[id] || {};
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
return this.#subscriberHandle?.webrtcStuff?.streamMap?.[id] || {};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#isSubscribedToMid(id, mid) {
|
|
603
|
+
if(this.#subscriberHandle) {
|
|
604
|
+
return !!this.#subscriberHandle.webrtcStuff.subscribeMap?.[id]?.[mid]
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#addToSubscribeMap(id, mid) {
|
|
610
|
+
if(this.#subscriberHandle) {
|
|
611
|
+
this.#subscriberHandle.webrtcStuff.subscribeMap[id] = this.#subscriberHandle.webrtcStuff.subscribeMap[id] || {};
|
|
612
|
+
this.#subscriberHandle.webrtcStuff.subscribeMap[id][mid] = true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#removeFromSubscribeMap(id, mid) {
|
|
617
|
+
if(this.#subscriberHandle) {
|
|
618
|
+
if(id && !mid) {
|
|
619
|
+
delete this.#subscriberHandle.webrtcStuff.subscribeMap[id];
|
|
620
|
+
}
|
|
621
|
+
else if(id && mid && this.#subscriberHandle.webrtcStuff.subscribeMap[id]) {
|
|
622
|
+
delete this.#subscriberHandle.webrtcStuff.subscribeMap[id][mid];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// will subscribe to all relevant participants
|
|
628
|
+
|
|
629
|
+
async #updateSubscriptions() {
|
|
630
|
+
|
|
631
|
+
// no subscriber handle, we create one and subscribe to relevant participants
|
|
632
|
+
|
|
633
|
+
if(!this.#subscriberJoinPromise) {
|
|
634
|
+
await this.#joinAsSubscriber(this.roomId, this.pin, this.userId);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
else {
|
|
638
|
+
|
|
639
|
+
// we have a subscriber handle, we need to check if we need to update subscriptions
|
|
640
|
+
// if subscriber handle is not ready yet, we wait for it to be ready
|
|
641
|
+
// if it fails, we try to create a new one
|
|
642
|
+
// if it succeeds, we check if we need to update subscriptions
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
await this.#subscriberJoinPromise;
|
|
646
|
+
} catch (e) {
|
|
647
|
+
await this.#joinAsSubscriber(this.roomId, this.pin, this.userId);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const handle = this.#subscriberHandle;
|
|
652
|
+
const subscribedTo = handle.webrtcStuff.subscribeMap || {};
|
|
653
|
+
const publishers = handle.webrtcStuff.availablePublishers;
|
|
654
|
+
const subscribe = [];
|
|
655
|
+
const unsubscribe = [];
|
|
656
|
+
const flatSourceMap = [];
|
|
657
|
+
|
|
658
|
+
for (let index in publishers) {
|
|
659
|
+
if(publishers[index]["dummy"])
|
|
660
|
+
continue;
|
|
661
|
+
|
|
662
|
+
let userId = publishers[index]["display"];
|
|
663
|
+
let id = publishers[index]["id"];
|
|
664
|
+
let streams = publishers[index]["streams"] || [];
|
|
665
|
+
let description = {};
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const dString = streams.find(s => !!s.description)?.description;
|
|
669
|
+
if(dString) {
|
|
670
|
+
description = JSON.parse(dString);
|
|
671
|
+
}
|
|
672
|
+
} catch (e) {}
|
|
673
|
+
|
|
674
|
+
this._log('Remote publisher: ', id, userId, streams, description);
|
|
675
|
+
|
|
676
|
+
for (let i in streams) {
|
|
677
|
+
const track = streams[i];
|
|
678
|
+
const mid = track.mid; // source.mid
|
|
679
|
+
flatSourceMap.push({id: id, mid: mid});
|
|
680
|
+
|
|
681
|
+
const isSubscribed = this.#isSubscribedToMid(id, mid);
|
|
682
|
+
|
|
683
|
+
if(track.disabled) {
|
|
684
|
+
if(isSubscribed) {
|
|
685
|
+
unsubscribe.push({feed: id, mid: mid});
|
|
686
|
+
this.#removeFromSubscribeMap(id, mid);
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if(!this.#shouldSubscribeParticipant(id, userId, description)) {
|
|
692
|
+
if(isSubscribed) {
|
|
693
|
+
unsubscribe.push({feed: id, mid: mid});
|
|
694
|
+
this.#removeFromSubscribeMap(id, mid);
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if(!isSubscribed) {
|
|
700
|
+
subscribe.push({feed: id, mid: mid});
|
|
701
|
+
this.#addToSubscribeMap(id, mid);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// check if we're subscribed to any mid that is no longer available in sources
|
|
707
|
+
|
|
708
|
+
Object.keys(subscribedTo).forEach(id => {
|
|
709
|
+
const mids = Object.keys(subscribedTo[id]);
|
|
710
|
+
for(let mid of mids) {
|
|
711
|
+
if(!flatSourceMap.find(s => s.id === id && s.mid === mid)) {
|
|
712
|
+
unsubscribe.push({feed: id, mid});
|
|
713
|
+
this.#removeFromSubscribeMap(id, mid);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
if(subscribe.length || unsubscribe.length) {
|
|
719
|
+
await this.sendMessage(handle.handleId, {
|
|
720
|
+
body: {
|
|
721
|
+
"request": "update",
|
|
722
|
+
...(subscribe.length ? {subscribe}: {}),
|
|
723
|
+
...(unsubscribe.length ? {unsubscribe}: {})
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#updatePublishersStreamMap() {
|
|
731
|
+
if(this.#subscriberHandle) {
|
|
732
|
+
let handle = this.#subscriberHandle;
|
|
733
|
+
handle.webrtcStuff.streamMap = {};
|
|
734
|
+
handle.webrtcStuff.transceiverMap.forEach(tItem => {
|
|
735
|
+
if(tItem.type === 'data') {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if(tItem.active === false) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const id = tItem.feed_id;
|
|
742
|
+
const source = JSON.parse(tItem.feed_description)?.source;
|
|
743
|
+
|
|
744
|
+
if(!handle.webrtcStuff.streamMap[id]) {
|
|
745
|
+
handle.webrtcStuff.streamMap[id] = {};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if(!handle.webrtcStuff.streamMap[id][source]) {
|
|
749
|
+
handle.webrtcStuff.streamMap[id][source] = [];
|
|
750
|
+
}
|
|
751
|
+
let trackId = handle.webrtcStuff.pc.getTransceivers().find(t => t.mid === tItem.mid)?.receiver?.track?.id;
|
|
752
|
+
|
|
753
|
+
if(trackId) {
|
|
754
|
+
handle.webrtcStuff.streamMap[id][source].push(trackId);
|
|
755
|
+
}
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#updateAvailablePublishersTrackData(newPublishers, removedPublisher) {
|
|
761
|
+
if(this.#subscriberHandle) {
|
|
762
|
+
for(let publisher of newPublishers) {
|
|
763
|
+
const publisherIndex = this.#subscriberHandle.webrtcStuff.availablePublishers.findIndex(p => p.id === publisher.id);
|
|
764
|
+
|
|
765
|
+
if(publisherIndex > -1) {
|
|
766
|
+
this.#subscriberHandle.webrtcStuff.availablePublishers.splice(publisherIndex, 1, publisher);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
this.#subscriberHandle.webrtcStuff.availablePublishers.push(publisher);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
}
|
|
773
|
+
if(removedPublisher !== undefined) {
|
|
774
|
+
this.#subscriberHandle.webrtcStuff.availablePublishers = this.#subscriberHandle.webrtcStuff.availablePublishers.filter(p => p.id !== removedPublisher);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#getTransceiverDataByMid(mid) {
|
|
780
|
+
return this.#subscriberHandle.webrtcStuff.transceiverMap.find(t => t.mid === mid);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// this is done
|
|
784
|
+
// feed_id === id of participant
|
|
785
|
+
|
|
786
|
+
#updateTransceiverMap(handleId, streams = []) {
|
|
787
|
+
this._log('Updating current transceiver map', 'Is me: ' + handleId === this.#publisherHandle.handleId, handleId, streams);
|
|
788
|
+
let handle = this.#getHandle(handleId);
|
|
789
|
+
if (!handle) {
|
|
790
|
+
this.emit('error', {
|
|
791
|
+
type: 'warning',
|
|
792
|
+
id: 1,
|
|
793
|
+
message: 'id non-existent',
|
|
794
|
+
data: [handleId, 'updateTransceiverMap']
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
let config = handle.webrtcStuff;
|
|
799
|
+
|
|
800
|
+
// we need to add new transceivers, update existing transceivers, remove transceivers that are not in streams anymore
|
|
801
|
+
const mids = []
|
|
802
|
+
for(let stream of streams) {
|
|
803
|
+
mids.push(stream.mid);
|
|
804
|
+
const index = config.transceiverMap.findIndex(t => t.mid === stream.mid);
|
|
805
|
+
if(index > -1) {
|
|
806
|
+
config.transceiverMap[index] = {...config.transceiverMap[index], ...stream};
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
config.transceiverMap.push(stream);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
config.transceiverMap = config.transceiverMap.filter(t => mids.includes(t.mid));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
#getParticipantEventName(adding = true,participantRole) {
|
|
816
|
+
if(adding) {
|
|
817
|
+
switch (participantRole) {
|
|
818
|
+
case 'participant':
|
|
819
|
+
return 'addRemoteParticipant';
|
|
820
|
+
case 'talkback':
|
|
821
|
+
return 'addRemoteTalkback';
|
|
822
|
+
case 'monitor':
|
|
823
|
+
return 'addRemoteTalkback';
|
|
824
|
+
case 'observer':
|
|
825
|
+
case 'observerSolo1':
|
|
826
|
+
case 'observerSolo2':
|
|
827
|
+
case 'observerSolo3':
|
|
828
|
+
case 'observerSolo4':
|
|
829
|
+
case 'observerSolo5':
|
|
830
|
+
return 'addRemoteObserver';
|
|
831
|
+
case 'host':
|
|
832
|
+
return 'addRemoteInstructor';
|
|
833
|
+
case 'companionTV':
|
|
834
|
+
return 'addRemoteCompanionTV';
|
|
835
|
+
case 'companionPhone':
|
|
836
|
+
return 'addRemoteCompanionPhone';
|
|
837
|
+
default:
|
|
838
|
+
return 'addRemoteParticipant';
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
switch (participantRole) {
|
|
843
|
+
case 'participant':
|
|
844
|
+
return 'removeRemoteParticipant';
|
|
845
|
+
case 'talkback':
|
|
846
|
+
return 'removeRemoteTalkback';
|
|
847
|
+
case 'monitor':
|
|
848
|
+
return 'removeRemoteTalkback';
|
|
849
|
+
case 'observer':
|
|
850
|
+
case 'observerSolo1':
|
|
851
|
+
case 'observerSolo2':
|
|
852
|
+
case 'observerSolo3':
|
|
853
|
+
case 'observerSolo4':
|
|
854
|
+
case 'observerSolo5':
|
|
855
|
+
return 'removeRemoteObserver';
|
|
856
|
+
case 'host':
|
|
857
|
+
return 'removeRemoteInstructor';
|
|
858
|
+
case 'companionTV':
|
|
859
|
+
return 'removeRemoteCompanionTV';
|
|
860
|
+
case 'companionPhone':
|
|
861
|
+
return 'removeRemoteCompanionPhone';
|
|
862
|
+
default:
|
|
863
|
+
return 'removeRemoteParticipant';
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async sendMessage(handleId, message = {body: 'Example Body'}, dontWait = false, dontResolveOnAck = false, retry = 0) {
|
|
869
|
+
return this.#send({
|
|
870
|
+
"janus": "message",
|
|
871
|
+
"handle_id": handleId,
|
|
872
|
+
...message
|
|
873
|
+
}, dontWait, dontResolveOnAck, retry)
|
|
874
|
+
.then(json => {
|
|
875
|
+
if (json && json["janus"] === "success") {
|
|
876
|
+
let plugindata = json["plugindata"] || {};
|
|
877
|
+
let data = plugindata["data"];
|
|
878
|
+
return Promise.resolve(data);
|
|
879
|
+
}
|
|
880
|
+
return Promise.resolve();
|
|
881
|
+
})
|
|
882
|
+
.catch(json => {
|
|
883
|
+
if (json && json["error"]) {
|
|
884
|
+
return Promise.reject({type: 'warning', id: 2, message: 'sendMessage failed', data: json["error"]})
|
|
885
|
+
} else {
|
|
886
|
+
return Promise.reject({type: 'warning', id: 3, message: 'sendMessage failed', data: json});
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async #sendHTTP(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
|
|
892
|
+
let transaction = RoomSession.randomString(12);
|
|
893
|
+
let requestData = {
|
|
894
|
+
...request,
|
|
895
|
+
transaction,
|
|
896
|
+
token: this.token,
|
|
897
|
+
...((this.sessionId && {'session_id': this.sessionId}) || {}),
|
|
898
|
+
...((this.apisecret && {'apisecret': this.apisecret}) || {})
|
|
899
|
+
};
|
|
900
|
+
this._log(requestData);
|
|
901
|
+
return this.#httpAPICall(this.server, {body:requestData})
|
|
902
|
+
.then(json => {
|
|
903
|
+
if(json['janus'] !== 'success' && json['janus'] !== 'ack') {
|
|
904
|
+
// not a success not an ack ... should be error
|
|
905
|
+
return Promise.reject(json["error"])
|
|
906
|
+
}
|
|
907
|
+
return json;
|
|
908
|
+
})
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async #sendWebsockets(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
|
|
912
|
+
let transaction = RoomSession.randomString(12);
|
|
913
|
+
let requestData = {
|
|
914
|
+
...request,
|
|
915
|
+
transaction,
|
|
916
|
+
token: this.token,
|
|
917
|
+
...((this.sessionId && {'session_id': this.sessionId}) || {}),
|
|
918
|
+
...((this.apisecret && {'apisecret': this.apisecret}) || {})
|
|
919
|
+
};
|
|
920
|
+
this._log(requestData);
|
|
921
|
+
const op = () => new Promise((resolve, reject) => {
|
|
922
|
+
let messageTimeoutId = null;
|
|
923
|
+
let abortResponse = () => {
|
|
924
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
925
|
+
clearTimeout(messageTimeoutId);
|
|
926
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
927
|
+
reject({type: 'warning', id: 4, message: 'connection cancelled'})
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
let parseResponse = (event) => {
|
|
931
|
+
let json = JSON.parse(event.data);
|
|
932
|
+
let r_transaction = json['transaction'];
|
|
933
|
+
if (r_transaction === transaction && (!dontResolveOnAck || json['janus'] !== 'ack')) {
|
|
934
|
+
clearTimeout(messageTimeoutId);
|
|
935
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
936
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
937
|
+
if (json['janus'] === 'error') {
|
|
938
|
+
if (json?.error?.code == 403) {
|
|
939
|
+
this.disconnect();
|
|
940
|
+
}
|
|
941
|
+
reject({type: 'error', id: 5, message: 'send failed', data: json, requestData});
|
|
942
|
+
} else {
|
|
943
|
+
resolve(json);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
if (ignoreResponse) {
|
|
949
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
950
|
+
this.ws.send(JSON.stringify(requestData));
|
|
951
|
+
}
|
|
952
|
+
resolve();
|
|
953
|
+
} else {
|
|
954
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
955
|
+
this.ws.addEventListener('message', parseResponse);
|
|
956
|
+
messageTimeoutId = setTimeout(() => {
|
|
957
|
+
this.ws.removeEventListener('message', parseResponse);
|
|
958
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
959
|
+
reject({type: 'warning', id: 6, message: 'send timeout', data: requestData});
|
|
960
|
+
}, this._sendMessageTimeout);
|
|
961
|
+
this._abortController.signal.addEventListener('abort', abortResponse);
|
|
962
|
+
this.ws.send(JSON.stringify(requestData));
|
|
963
|
+
} else {
|
|
964
|
+
reject({type: 'warning', id: 7, message: 'No connection to WebSockets', data: requestData});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
return op().catch(e => {
|
|
970
|
+
if (e.id === 17) {
|
|
971
|
+
return Promise.reject(e);
|
|
972
|
+
}
|
|
973
|
+
else if(e.id === 29 && retry > 0) {
|
|
974
|
+
return wait(this._sendMessageTimeout).then(() => this.#send(request, ignoreResponse, dontResolveOnAck, retry - 1));
|
|
975
|
+
}
|
|
976
|
+
else if(retry > 0) {
|
|
977
|
+
return this.#send(request, ignoreResponse, dontResolveOnAck, retry - 1);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
return Promise.reject(e);
|
|
981
|
+
}
|
|
982
|
+
})
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async #send(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
|
|
986
|
+
if(this.useWebsockets) {
|
|
987
|
+
return this.#sendWebsockets(request, ignoreResponse, dontResolveOnAck, retry);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
return this.#sendHTTP(request, ignoreResponse, dontResolveOnAck, retry)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
#longPoll() {
|
|
995
|
+
|
|
996
|
+
if(!this.isSupposeToBeConnected) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
let longpoll = this.server + "/" + this.sessionId + "?rid=" + new Date().getTime();
|
|
1001
|
+
if(this._maxev)
|
|
1002
|
+
longpoll = longpoll + "&maxev=" + this._maxev;
|
|
1003
|
+
if(this.token)
|
|
1004
|
+
longpoll = longpoll + "&token=" + encodeURIComponent(this.token);
|
|
1005
|
+
if(this.apisecret)
|
|
1006
|
+
longpoll = longpoll + "&apisecret=" + encodeURIComponent(this.apisecret);
|
|
1007
|
+
|
|
1008
|
+
this.#httpAPICall(longpoll, {
|
|
1009
|
+
verb: 'GET',
|
|
1010
|
+
timeout: this._longPollTimeout
|
|
1011
|
+
})
|
|
1012
|
+
.then(this.#handleWsEvents.bind(this))
|
|
1013
|
+
.catch(() => {
|
|
1014
|
+
this.#longPoll();
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
#connectionClosed() {
|
|
1019
|
+
|
|
1020
|
+
if (!this.isConnected || this.isConnecting || this.isDisconnecting) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
this.#reconnect()
|
|
1025
|
+
.catch(e => {
|
|
1026
|
+
if(e.type !== 'warning') {
|
|
1027
|
+
this.disconnect();
|
|
1028
|
+
}
|
|
1029
|
+
this.emit('error', e)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
#wipeListeners() {
|
|
1034
|
+
if (this.ws) {
|
|
1035
|
+
this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
|
|
1036
|
+
this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
#startKeepAlive() {
|
|
1041
|
+
this.#send({"janus": "keepalive"})
|
|
1042
|
+
.then(json => {
|
|
1043
|
+
if (json["janus"] !== 'ack') {
|
|
1044
|
+
this.emit('error', {
|
|
1045
|
+
type: 'warning',
|
|
1046
|
+
id: 8,
|
|
1047
|
+
message: 'keepalive response suspicious',
|
|
1048
|
+
data: json["janus"]
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
.catch((e) => {
|
|
1053
|
+
this.emit('error', {type: 'warning', id: 9, message: 'keepalive dead', data: e});
|
|
1054
|
+
this.#connectionClosed();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
this._keepAliveId = setTimeout(() => {
|
|
1058
|
+
this.#startKeepAlive();
|
|
1059
|
+
}, this._keepAlivePeriod);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
#stopKeepAlive() {
|
|
1063
|
+
clearTimeout(this._keepAliveId);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
#handleWsEvents(event, skipPoll = false) {
|
|
1067
|
+
|
|
1068
|
+
// we have a response from long poll, that means we need to run it again
|
|
1069
|
+
if(!this.useWebsockets && !skipPoll) {
|
|
1070
|
+
this.#longPoll();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if(!this.useWebsockets && skipPoll) {
|
|
1074
|
+
// we need to fire those events into the wild
|
|
1075
|
+
this.emit('longPollEvent', event);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if(Array.isArray(event)) {
|
|
1079
|
+
event.forEach(ev => this.#handleWsEvents({data:ev}, true));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
let json = typeof event.data === 'string'
|
|
1084
|
+
? JSON.parse(event.data)
|
|
1085
|
+
: event.data;
|
|
1086
|
+
|
|
1087
|
+
var sender = json["sender"];
|
|
1088
|
+
var type = json["janus"];
|
|
1089
|
+
|
|
1090
|
+
let handle = this.#getHandle(sender);
|
|
1091
|
+
if (!handle) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (type === "trickle") {
|
|
1096
|
+
let candidate = json["candidate"];
|
|
1097
|
+
let config = handle.webrtcStuff;
|
|
1098
|
+
if (config.pc && config.remoteSdp) {
|
|
1099
|
+
|
|
1100
|
+
if (!candidate || candidate.completed === true) {
|
|
1101
|
+
config.pc.addIceCandidate(null).catch((e) => {
|
|
1102
|
+
this._log('Error adding null candidate', e);
|
|
1103
|
+
});
|
|
1104
|
+
} else {
|
|
1105
|
+
config.pc.addIceCandidate(candidate).catch((e) => {
|
|
1106
|
+
this._log('Error adding candidate', e);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
if (!config.candidates) {
|
|
1111
|
+
config.candidates = [];
|
|
1112
|
+
}
|
|
1113
|
+
config.candidates.push(candidate);
|
|
1114
|
+
}
|
|
1115
|
+
} else if (type === "webrtcup") {
|
|
1116
|
+
//none universal
|
|
1117
|
+
} else if (type === "hangup") {
|
|
1118
|
+
this._log('hangup on', handle.handleId, handle.handleId === this.#publisherHandle.handleId, json);
|
|
1119
|
+
if(this.#getHandle(handle.handleId)) {
|
|
1120
|
+
this.#cleanupHandle(handle.handleId, true).catch(() => {});
|
|
1121
|
+
}
|
|
1122
|
+
} else if (type === "detached") {
|
|
1123
|
+
this._log('detached on', handle.handleId, handle.handleId === this.#publisherHandle.handleId, json);
|
|
1124
|
+
if(this.#getHandle(handle.handleId)) {
|
|
1125
|
+
this.#destroyHandle(handle.handleId).catch(() => {});
|
|
1126
|
+
}
|
|
1127
|
+
} else if (type === "media") {
|
|
1128
|
+
this._log('Media event:', handle.handleId, json["type"], json["receiving"], json["mid"]);
|
|
1129
|
+
} else if (type === "slowlink") {
|
|
1130
|
+
this._log('Slowlink', handle.handleId, json["uplink"], json["lost"], json["mid"]);
|
|
1131
|
+
} else if (type === "event") {
|
|
1132
|
+
//none universal
|
|
1133
|
+
} else if (type === 'timeout') {
|
|
1134
|
+
this._log('WebSockets Gateway timeout', json);
|
|
1135
|
+
this.ws.close(3504, "Gateway timeout");
|
|
1136
|
+
} else if (type === 'success' || type === 'error') {
|
|
1137
|
+
// we're capturing those elsewhere
|
|
1138
|
+
} else {
|
|
1139
|
+
this._log(`Unknown event: ${type} on session: ${this.sessionId}`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
// LOCAL
|
|
1144
|
+
|
|
1145
|
+
if (sender === this.#publisherHandle.handleId) {
|
|
1146
|
+
if (type === "event") {
|
|
1147
|
+
|
|
1148
|
+
var plugindata = json["plugindata"] || {};
|
|
1149
|
+
var msg = plugindata["data"] || {};
|
|
1150
|
+
var jsep = json["jsep"];
|
|
1151
|
+
let result = msg["result"] || null; // related to streaming plugin
|
|
1152
|
+
let event = msg["videoroom"] || null;
|
|
1153
|
+
let attendees = msg["attendees"] ?? [];
|
|
1154
|
+
let list = msg["publishers"] || [];
|
|
1155
|
+
let leaving = msg["leaving"];
|
|
1156
|
+
let kicked = msg["kicked"];
|
|
1157
|
+
let substream = msg["substream"];
|
|
1158
|
+
let temporal = msg["temporal"] ?? msg["temporal_layer"];
|
|
1159
|
+
let spatial = msg["spatial_layer"];
|
|
1160
|
+
let joining = msg["joining"];
|
|
1161
|
+
let unpublished = msg["unpublished"];
|
|
1162
|
+
let error = msg["error"];
|
|
1163
|
+
|
|
1164
|
+
if (event === "joined") {
|
|
1165
|
+
|
|
1166
|
+
this.id = msg["id"];
|
|
1167
|
+
this.privateId = msg["private_id"];
|
|
1168
|
+
this.isConnected = true;
|
|
1169
|
+
|
|
1170
|
+
this._log('We have successfully joined Room',msg);
|
|
1171
|
+
this.emit('joined', true, this.constructId);
|
|
1172
|
+
// initial events
|
|
1173
|
+
|
|
1174
|
+
attendees.forEach((attendee) => {
|
|
1175
|
+
this.#cacheAttendee(attendee);
|
|
1176
|
+
this.#emitRemoteFeedUpdate(null, {attendee});
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
list.forEach((publisher) => {
|
|
1180
|
+
this.#cacheAttendee(publisher);
|
|
1181
|
+
this.#emitRemoteFeedUpdate(null, {attendee: publisher});
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
// end of initial events
|
|
1185
|
+
this.#emitLocalFeedUpdate();
|
|
1186
|
+
this.#updateAvailablePublishersTrackData(list);
|
|
1187
|
+
this.#updateSubscriptions()
|
|
1188
|
+
.catch((e) => {
|
|
1189
|
+
this._log(e);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
}
|
|
1193
|
+
else if (event === "event") {
|
|
1194
|
+
|
|
1195
|
+
if(substream !== undefined && substream !== null) {
|
|
1196
|
+
this._log('Substream event:', substream, sender);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if(temporal !== undefined && temporal !== null) {
|
|
1200
|
+
this._log('Temporal event:', temporal);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (msg["streams"] !== undefined && msg["streams"] !== null) {
|
|
1204
|
+
this._log('Got my own streams back', msg["streams"]);
|
|
1205
|
+
this.#updateTransceiverMap(handle.handleId, msg["streams"]);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
this.#updateAvailablePublishersTrackData(list);
|
|
1209
|
+
this.#updateSubscriptions()
|
|
1210
|
+
.catch((e) => {
|
|
1211
|
+
this._log(e);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
if (leaving === 'ok') {
|
|
1216
|
+
this._log('leaving', this.#publisherHandle.handleId, 'this is us');
|
|
1217
|
+
|
|
1218
|
+
this.#emitLocalFeedUpdate({feedRemoval: true});
|
|
1219
|
+
|
|
1220
|
+
if (msg['reason'] === 'kicked') {
|
|
1221
|
+
this.emit('kicked');
|
|
1222
|
+
this.disconnect().catch(() => {});
|
|
1223
|
+
}
|
|
1224
|
+
} else if (leaving) {
|
|
1225
|
+
|
|
1226
|
+
this._log('leaving', leaving);
|
|
1227
|
+
this.#updateAvailablePublishersTrackData([], leaving);
|
|
1228
|
+
this.#updateSubscriptions()
|
|
1229
|
+
.catch((e) => {
|
|
1230
|
+
this._log(e);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id:leaving}});
|
|
1234
|
+
this.#removeAttendeeFromCache(leaving);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (unpublished === 'ok') {
|
|
1238
|
+
this._log('unpublished', this.#publisherHandle.handleId, 'this is us');
|
|
1239
|
+
this.#cleanupHandle(this.#publisherHandle.handleId, true).catch(() => {});
|
|
1240
|
+
|
|
1241
|
+
} else if (unpublished) {
|
|
1242
|
+
this._log('unpublished', unpublished);
|
|
1243
|
+
this.#updateAvailablePublishersTrackData([], unpublished);
|
|
1244
|
+
this.#updateSubscriptions()
|
|
1245
|
+
.catch((e) => {
|
|
1246
|
+
this._log(e);
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if(kicked === 'ok') {
|
|
1251
|
+
// this case shouldn't exist
|
|
1252
|
+
} else if(kicked) {
|
|
1253
|
+
this._log('kicked', kicked);
|
|
1254
|
+
this.#updateAvailablePublishersTrackData([], kicked);
|
|
1255
|
+
this.#updateSubscriptions()
|
|
1256
|
+
.catch((e) => {
|
|
1257
|
+
this._log(e);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if(joining) {
|
|
1262
|
+
this.#cacheAttendee(joining);
|
|
1263
|
+
this.#emitRemoteFeedUpdate(null, {attendee: joining});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (error) {
|
|
1267
|
+
this.emit('error', {
|
|
1268
|
+
type: 'error',
|
|
1269
|
+
id: 10,
|
|
1270
|
+
message: 'local participant error',
|
|
1271
|
+
data: [sender, msg]
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (jsep !== undefined && jsep !== null) {
|
|
1277
|
+
this.#webrtcPeer(this.#publisherHandle.handleId, jsep)
|
|
1278
|
+
.catch(err => {
|
|
1279
|
+
this.emit('error', err);
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
else if (type === "webrtcup") {
|
|
1284
|
+
|
|
1285
|
+
if(this.simulcast) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
this._log('Configuring bitrate: ' + this.initialBitrate);
|
|
1290
|
+
if (this.initialBitrate > 0) {
|
|
1291
|
+
this.sendMessage(this.#publisherHandle.handleId, {
|
|
1292
|
+
"body": {
|
|
1293
|
+
"request": "configure",
|
|
1294
|
+
"bitrate": this.initialBitrate
|
|
1295
|
+
}
|
|
1296
|
+
}).catch(() => null)
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
//REMOTE
|
|
1302
|
+
|
|
1303
|
+
else {
|
|
1304
|
+
|
|
1305
|
+
let plugindata = json["plugindata"] || {};
|
|
1306
|
+
let msg = plugindata["data"] || {};
|
|
1307
|
+
let jsep = json["jsep"];
|
|
1308
|
+
let event = msg["videoroom"];
|
|
1309
|
+
let error = msg["error"];
|
|
1310
|
+
let substream = msg["substream"];
|
|
1311
|
+
let temporal = msg["temporal"] ?? msg["temporal_layer"];
|
|
1312
|
+
let spatial = msg["spatial_layer"];
|
|
1313
|
+
let mid = msg["mid"];
|
|
1314
|
+
|
|
1315
|
+
if(substream !== undefined && substream !== null) {
|
|
1316
|
+
this._log('Substream: ', sender, mid, substream);
|
|
1317
|
+
this.#setSelectedSubstream(sender, mid, substream);
|
|
1318
|
+
this.requestKeyFrame(sender, mid);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if(temporal !== undefined && temporal !== null) {
|
|
1322
|
+
this._log('Temporal: ', temporal);
|
|
1323
|
+
this.#setSelectedTemporal(sender, mid, temporal);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if(type === "webrtcup") {
|
|
1327
|
+
this.requestKeyFrame(handle.handleId);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (event === "updated") {
|
|
1331
|
+
this._log('Remote has updated tracks', msg);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (event === "attached") {
|
|
1335
|
+
this._log('Subscriber handle successfuly created', msg);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if(msg["streams"]) {
|
|
1339
|
+
this.#updateTransceiverMap(handle.handleId, msg["streams"]);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (error) {
|
|
1343
|
+
this.emit('error', {type: 'warning', id: 11, message: 'remote participant error', data: [sender, msg]});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (jsep) {
|
|
1347
|
+
this.#publishRemote(handle.handleId, jsep)
|
|
1348
|
+
.catch(err => {
|
|
1349
|
+
this.emit('error', err);
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
#handleDataChannelEvents(handleId, type, data) {
|
|
1356
|
+
|
|
1357
|
+
let handle = this.#getHandle(handleId);
|
|
1358
|
+
|
|
1359
|
+
if (type === 'state') {
|
|
1360
|
+
this._log(` - Data channel status - `, `UID: ${handleId}`, `STATUS: ${JSON.stringify(data)}`)
|
|
1361
|
+
if (handle) {
|
|
1362
|
+
let config = handle.webrtcStuff;
|
|
1363
|
+
config.dataChannelOpen = this.defaultDataChannelLabel === data?.label && data?.state === 'open';
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (handleId === this.#publisherHandle.handleId && this.defaultDataChannelLabel === data?.label) {
|
|
1367
|
+
this._isDataChannelOpen = data?.state === 'open';
|
|
1368
|
+
this.emit('dataChannel', data?.state === 'open');
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (type === 'error') {
|
|
1373
|
+
|
|
1374
|
+
this.emit('error', {
|
|
1375
|
+
type: 'warning',
|
|
1376
|
+
id: 12,
|
|
1377
|
+
message: 'data event warning',
|
|
1378
|
+
data: [handleId, data]
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
if (handle) {
|
|
1382
|
+
let config = handle.webrtcStuff;
|
|
1383
|
+
if(this.defaultDataChannelLabel === data.label) {
|
|
1384
|
+
config.dataChannelOpen = false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (handleId === this.#publisherHandle.handleId && this.defaultDataChannelLabel === data.label) {
|
|
1388
|
+
this._isDataChannelOpen = false;
|
|
1389
|
+
this.emit('dataChannel', false);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (handleId === this.#publisherHandle.handleId && type === 'message') {
|
|
1394
|
+
|
|
1395
|
+
let d = null;
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
d = JSON.parse(data)
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
this.emit('error', {type: 'warning', id: 13, message: 'data message parse error', data: [handleId, e]});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
this.emit('data', d);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async #createHandle() {
|
|
1409
|
+
return this.#send({
|
|
1410
|
+
"janus": "attach",
|
|
1411
|
+
"plugin": this.pluginName,
|
|
1412
|
+
}).then(json => {
|
|
1413
|
+
let handleId = json.data["id"];
|
|
1414
|
+
return {
|
|
1415
|
+
handleId,
|
|
1416
|
+
webrtcStuff: {
|
|
1417
|
+
mySdp: null,
|
|
1418
|
+
mediaConstraints: null,
|
|
1419
|
+
pc: null,
|
|
1420
|
+
dataChannelOpen: false,
|
|
1421
|
+
dataChannel: null,
|
|
1422
|
+
dtmfSender: null,
|
|
1423
|
+
trickle: true,
|
|
1424
|
+
iceDone: false,
|
|
1425
|
+
isIceRestarting: false,
|
|
1426
|
+
tracks: [],
|
|
1427
|
+
stream: new MediaStream(), // for publisher handle only
|
|
1428
|
+
mids: {},
|
|
1429
|
+
userIdToDisplay: {}, // userId to display
|
|
1430
|
+
transceiverMap: [], // got from msg["streams"]
|
|
1431
|
+
streamMap: {}, // id to sources to mids?
|
|
1432
|
+
availablePublishers: [],
|
|
1433
|
+
subscribeMap: [], // subscribed to [id][mid]
|
|
1434
|
+
stats: {},
|
|
1435
|
+
currentLayers : new Map(),
|
|
1436
|
+
qualityHistory : new Map(),
|
|
1437
|
+
lastSwitchTime : new Map(),
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
})
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
async #destroyHandle(handleId, detachRequest = true) {
|
|
1444
|
+
if(handleId) {
|
|
1445
|
+
let handle = this.#getHandle(handleId);
|
|
1446
|
+
if (handle) {
|
|
1447
|
+
|
|
1448
|
+
await this.#cleanupHandle(handleId);
|
|
1449
|
+
|
|
1450
|
+
if(detachRequest) {
|
|
1451
|
+
await this.#send({
|
|
1452
|
+
"janus": "detach",
|
|
1453
|
+
"handle_id": handleId
|
|
1454
|
+
}, true)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (handleId === this.#publisherHandle?.handleId) {
|
|
1458
|
+
this.#publisherHandle = null;
|
|
1459
|
+
}
|
|
1460
|
+
else if (handleId === this.#subscriberHandle?.handleId) {
|
|
1461
|
+
this.#subscriberHandle = null;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async #cleanupHandle(handleId, hangupRequest = false) {
|
|
1468
|
+
|
|
1469
|
+
let handle = this.#getHandle(handleId);
|
|
1470
|
+
|
|
1471
|
+
if(hangupRequest) {
|
|
1472
|
+
await this.#send({"janus": "hangup", "handle_id": handleId,}, true)
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
try {
|
|
1476
|
+
if (handle.webrtcStuff.stream) {
|
|
1477
|
+
if(!this.isConnecting) {
|
|
1478
|
+
handle.webrtcStuff.stream?.getTracks().forEach(track => track.stop());
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
handle.webrtcStuff.stream?.getTracks().forEach(track => track.onended = null);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if(handle.webrtcStuff?.tracks?.length > 0) {
|
|
1485
|
+
handle.webrtcStuff.tracks.forEach(track => {
|
|
1486
|
+
if (track) {
|
|
1487
|
+
track.stop();
|
|
1488
|
+
track.onended = null;
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
// Do nothing
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if(handle.webrtcStuff.stream) {
|
|
1497
|
+
handle.webrtcStuff.stream.onremovetrack = null;
|
|
1498
|
+
handle.webrtcStuff.stream = null;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (handle.webrtcStuff.dataChannel) {
|
|
1502
|
+
Object.keys(handle.webrtcStuff.dataChannel).forEach(label => {
|
|
1503
|
+
handle.webrtcStuff.dataChannel[label].onmessage = null;
|
|
1504
|
+
handle.webrtcStuff.dataChannel[label].onopen = null;
|
|
1505
|
+
handle.webrtcStuff.dataChannel[label].onclose = null;
|
|
1506
|
+
handle.webrtcStuff.dataChannel[label].onerror = null;
|
|
1507
|
+
})
|
|
1508
|
+
}
|
|
1509
|
+
if (handle.webrtcStuff.pc) {
|
|
1510
|
+
handle.webrtcStuff.pc.onicecandidate = null;
|
|
1511
|
+
handle.webrtcStuff.pc.ontrack = null;
|
|
1512
|
+
handle.webrtcStuff.pc.ondatachannel = null;
|
|
1513
|
+
handle.webrtcStuff.pc.onconnectionstatechange = null;
|
|
1514
|
+
handle.webrtcStuff.pc.oniceconnectionstatechange = null;
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
handle.webrtcStuff.pc.close();
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
|
|
1520
|
+
}
|
|
1521
|
+
handle.webrtcStuff = {
|
|
1522
|
+
mySdp: null,
|
|
1523
|
+
mediaConstraints: null,
|
|
1524
|
+
pc: null,
|
|
1525
|
+
dataChannelOpen: false,
|
|
1526
|
+
dataChannel: null,
|
|
1527
|
+
dtmfSender: null,
|
|
1528
|
+
trickle: true,
|
|
1529
|
+
iceDone: false,
|
|
1530
|
+
isIceRestarting: false,
|
|
1531
|
+
tracks: [],
|
|
1532
|
+
stream: new MediaStream(), // for publisher handle only
|
|
1533
|
+
mids: {},
|
|
1534
|
+
userIdToDisplay: {}, // userId to display
|
|
1535
|
+
transceiverMap: [], // got from msg["streams"]
|
|
1536
|
+
streamMap: {}, // id to sources to mids?
|
|
1537
|
+
availablePublishers: [],
|
|
1538
|
+
subscribeMap: [], // subscribed to [id][mid]
|
|
1539
|
+
stats: {},
|
|
1540
|
+
currentLayers : new Map(),
|
|
1541
|
+
qualityHistory : new Map(),
|
|
1542
|
+
lastSwitchTime : new Map(),
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if(this.#publisherHandle && handleId === this.#publisherHandle.handleId) {
|
|
1546
|
+
this.#emitLocalFeedUpdate({feedRemoval: true});
|
|
1547
|
+
}
|
|
1548
|
+
else if(this.#subscriberHandle && handleId === this.#subscriberHandle.handleId) {
|
|
1549
|
+
[...this.#getCachedAttendeeIds()].forEach(id => {
|
|
1550
|
+
this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id}});
|
|
1551
|
+
this.#removeAttendeeFromCache(id);
|
|
1552
|
+
})
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
async #joinAsPublisher(roomId, pin, userId, display) {
|
|
1558
|
+
return this.sendMessage(this.#publisherHandle.handleId, {
|
|
1559
|
+
body: {
|
|
1560
|
+
"request": "join", "room": roomId, "pin": pin, "ptype": "publisher", "display": display, ...(this.webrtcVersion > 1000 ? {id: userId} : {})
|
|
1561
|
+
}
|
|
1562
|
+
}, false, true)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async #joinAsSubscriber(roomId, pin, userId) {
|
|
1566
|
+
|
|
1567
|
+
const publishers = this.#subscriberHandle.webrtcStuff.availablePublishers;
|
|
1568
|
+
|
|
1569
|
+
if(!publishers.length) {
|
|
1570
|
+
return Promise.reject('no streams to subscribe to');
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if(this.#subscriberJoinPromise) {
|
|
1574
|
+
return this.#subscriberJoinPromise;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
let res_streams = [];
|
|
1578
|
+
for (let f in publishers) {
|
|
1579
|
+
|
|
1580
|
+
if(publishers[f]["dummy"])
|
|
1581
|
+
continue;
|
|
1582
|
+
|
|
1583
|
+
let userId = publishers[f]["display"];
|
|
1584
|
+
let streams = publishers[f]["streams"] || [];
|
|
1585
|
+
let id = publishers[f]["id"];
|
|
1586
|
+
let description = {};
|
|
1587
|
+
|
|
1588
|
+
try {
|
|
1589
|
+
const dString = streams.find(s => !!s.description)?.description;
|
|
1590
|
+
if(dString) {
|
|
1591
|
+
description = JSON.parse(dString);
|
|
1592
|
+
}
|
|
1593
|
+
} catch (e) {}
|
|
1594
|
+
|
|
1595
|
+
this._log('Remote publisher: ', id, userId, streams, description);
|
|
1596
|
+
|
|
1597
|
+
for (let i in streams) {
|
|
1598
|
+
const track = streams[i];
|
|
1599
|
+
const mid = track.mid; // source.mid
|
|
1600
|
+
if(track.disabled) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if(!this.#shouldSubscribeParticipant(id, userId, description)) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if(!this.#isSubscribedToMid(id, mid)) {
|
|
1609
|
+
res_streams.push({
|
|
1610
|
+
feed: id, // This is mandatory
|
|
1611
|
+
mid: mid
|
|
1612
|
+
});
|
|
1613
|
+
this.#addToSubscribeMap(id, mid);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if(!res_streams.length) {
|
|
1619
|
+
return Promise.reject('no streams to subscribe to');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
this.#subscriberJoinPromise = this.sendMessage(this.#subscriberHandle.handleId, {
|
|
1623
|
+
body: {
|
|
1624
|
+
"request": "join",
|
|
1625
|
+
"room": roomId,
|
|
1626
|
+
"ptype": "subscriber",
|
|
1627
|
+
"use_msid": true,
|
|
1628
|
+
"private_id": this.privateId,
|
|
1629
|
+
streams: res_streams,
|
|
1630
|
+
...(this.webrtcVersion > 1000 ? {id: userId} : {}),
|
|
1631
|
+
pin: pin
|
|
1632
|
+
}
|
|
1633
|
+
}, false, true)
|
|
1634
|
+
.catch((e) => {
|
|
1635
|
+
this._log(e);
|
|
1636
|
+
this.#subscriberJoinPromise = null;
|
|
1637
|
+
return Promise.reject(e);
|
|
1638
|
+
})
|
|
1639
|
+
|
|
1640
|
+
return this.#subscriberJoinPromise;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async #leaveRoom(dontWait = false) {
|
|
1644
|
+
return this.isConnected
|
|
1645
|
+
? this.sendMessage(this.#publisherHandle.handleId, {body: {"request": "leave"}}, dontWait)
|
|
1646
|
+
.finally(() => {
|
|
1647
|
+
this.isConnected = false;
|
|
1648
|
+
this.emit('joined', false);
|
|
1649
|
+
})
|
|
1650
|
+
: Promise.resolve();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
async #webSocketConnection(reclaim = false) {
|
|
1654
|
+
|
|
1655
|
+
if(this.isEstablishingConnection) {
|
|
1656
|
+
return Promise.reject({type: 'warning', id: 14, message: 'connection already in progress'});
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
this.isEstablishingConnection = true;
|
|
1660
|
+
|
|
1661
|
+
if (this.ws) {
|
|
1662
|
+
this.#wipeListeners();
|
|
1663
|
+
if (this.ws.readyState === 1) {
|
|
1664
|
+
this.ws.close();
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
this.#stopKeepAlive();
|
|
1669
|
+
|
|
1670
|
+
return new Promise((resolve, reject) => {
|
|
1671
|
+
|
|
1672
|
+
this.__connectionClosedBoundFn = this.#connectionClosed.bind(this);
|
|
1673
|
+
this.__handleWsEventsBoundFn = this.#handleWsEvents.bind(this);
|
|
1674
|
+
|
|
1675
|
+
let abortConnect = () => {
|
|
1676
|
+
this._abortController.signal.removeEventListener('abort', abortConnect);
|
|
1677
|
+
this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
|
|
1678
|
+
this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
|
|
1679
|
+
this.ws.onopen = null;
|
|
1680
|
+
this.ws.onerror = null;
|
|
1681
|
+
reject({type: 'warning', id: 15, message: 'Connection cancelled'});
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
this.ws = new WebSocket(this.server, 'janus-protocol');
|
|
1685
|
+
this.ws.addEventListener('close', this.__connectionClosedBoundFn);
|
|
1686
|
+
this.ws.addEventListener('message', this.__handleWsEventsBoundFn);
|
|
1687
|
+
|
|
1688
|
+
this.ws.onopen = () => {
|
|
1689
|
+
this._abortController.signal.removeEventListener('abort', abortConnect);
|
|
1690
|
+
if(!reclaim) {
|
|
1691
|
+
this.#send({"janus": "create"})
|
|
1692
|
+
.then(json => {
|
|
1693
|
+
this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
|
|
1694
|
+
this.#startKeepAlive();
|
|
1695
|
+
resolve(this);
|
|
1696
|
+
})
|
|
1697
|
+
.catch(error => {
|
|
1698
|
+
reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 16, message: 'connection error', data: error})
|
|
1699
|
+
})
|
|
1700
|
+
.finally(() => {
|
|
1701
|
+
this.isEstablishingConnection = false;
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
this.#send({"janus": "claim"})
|
|
1706
|
+
.then(json => {
|
|
1707
|
+
this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
|
|
1708
|
+
this.#startKeepAlive();
|
|
1709
|
+
resolve(json);
|
|
1710
|
+
})
|
|
1711
|
+
.catch(error => {
|
|
1712
|
+
reject({type: 'error', id: 17, message: 'reconnection error', data: error})
|
|
1713
|
+
})
|
|
1714
|
+
.finally(() => {
|
|
1715
|
+
this.isEstablishingConnection = false;
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
this.ws.onerror = (e) => {
|
|
1721
|
+
this._abortController.signal.removeEventListener('abort', abortConnect);
|
|
1722
|
+
reject({type: 'error', id: 18, message: 'ws connection error', data: e});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
this._abortController.signal.addEventListener('abort', abortConnect);
|
|
1726
|
+
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
async #httpConnection(reclaim = false) {
|
|
1731
|
+
|
|
1732
|
+
if(this.isEstablishingConnection) {
|
|
1733
|
+
this.emit('error', {type: 'warning', id: 19, message: 'connection already in progress'});
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
this.isEstablishingConnection = true;
|
|
1738
|
+
|
|
1739
|
+
// usable when switching from ws to http
|
|
1740
|
+
if (this.ws) {
|
|
1741
|
+
this.#wipeListeners();
|
|
1742
|
+
if (this.ws.readyState === 1) {
|
|
1743
|
+
this.ws.close();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
this.#stopKeepAlive();
|
|
1748
|
+
|
|
1749
|
+
if(!reclaim) {
|
|
1750
|
+
return this.#send({"janus": "create"})
|
|
1751
|
+
.then(json => {
|
|
1752
|
+
this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
|
|
1753
|
+
this.#startKeepAlive();
|
|
1754
|
+
this.#longPoll();
|
|
1755
|
+
return this;
|
|
1756
|
+
})
|
|
1757
|
+
.catch(error => {
|
|
1758
|
+
return Promise.reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 20, message: 'connection error', data: error})
|
|
1759
|
+
})
|
|
1760
|
+
.finally(() => {
|
|
1761
|
+
this.isEstablishingConnection = false;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
else {
|
|
1765
|
+
return this.#send({"janus": "claim"})
|
|
1766
|
+
.then(json => {
|
|
1767
|
+
this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
|
|
1768
|
+
this.#startKeepAlive();
|
|
1769
|
+
this.#longPoll();
|
|
1770
|
+
return this;
|
|
1771
|
+
})
|
|
1772
|
+
.catch(error => {
|
|
1773
|
+
return Promise.reject({type: 'error', id: 21, message: 'reconnection error', data: error})
|
|
1774
|
+
})
|
|
1775
|
+
.finally(() => {
|
|
1776
|
+
this.isEstablishingConnection = false;
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async #waitForConnectEvent() {
|
|
1782
|
+
return new Promise((resolve, reject) => {
|
|
1783
|
+
let timeoutId = null;
|
|
1784
|
+
if(this.isConnected) {
|
|
1785
|
+
return resolve();
|
|
1786
|
+
}
|
|
1787
|
+
const cleanup = () => {
|
|
1788
|
+
clearTimeout(timeoutId);
|
|
1789
|
+
this.off('joined', _resolve);
|
|
1790
|
+
this._abortController.signal.removeEventListener('abort', _rejectAbort);
|
|
1791
|
+
}
|
|
1792
|
+
let _resolve = () => {
|
|
1793
|
+
cleanup();
|
|
1794
|
+
resolve();
|
|
1795
|
+
}
|
|
1796
|
+
let _rejectAbort = () => {
|
|
1797
|
+
cleanup();
|
|
1798
|
+
reject({type: 'warning', id: 43, message: 'Connection cancelled'})
|
|
1799
|
+
}
|
|
1800
|
+
let _rejectTimeout = () => {
|
|
1801
|
+
cleanup();
|
|
1802
|
+
reject({type: 'warning', id: 44, message: 'Connection timeout'})
|
|
1803
|
+
}
|
|
1804
|
+
this.once('joined', _resolve);
|
|
1805
|
+
timeoutId = setTimeout(_rejectTimeout, 10000)
|
|
1806
|
+
this._abortController.signal.addEventListener('abort', _rejectAbort);
|
|
1807
|
+
})
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async #reconnect() {
|
|
1811
|
+
|
|
1812
|
+
if (this.isConnecting) {
|
|
1813
|
+
return Promise.reject({type: 'warning', id: 22, message: 'connection already in progress'});
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
this.isConnecting = true;
|
|
1817
|
+
|
|
1818
|
+
if(this.useWebsockets) {
|
|
1819
|
+
try {
|
|
1820
|
+
await this.#webSocketConnection(true)
|
|
1821
|
+
}
|
|
1822
|
+
finally {
|
|
1823
|
+
this.isConnecting = false;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
try {
|
|
1828
|
+
await this.#httpConnection(true)
|
|
1829
|
+
}
|
|
1830
|
+
finally {
|
|
1831
|
+
this.isConnecting = false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async connect(
|
|
1837
|
+
roomId,
|
|
1838
|
+
pin,
|
|
1839
|
+
server,
|
|
1840
|
+
protocol,
|
|
1841
|
+
iceServers,
|
|
1842
|
+
token,
|
|
1843
|
+
display,
|
|
1844
|
+
userId,
|
|
1845
|
+
webrtcVersion = 0,
|
|
1846
|
+
initialBitrate = 0,
|
|
1847
|
+
recordingFilename,
|
|
1848
|
+
simulcast = false,
|
|
1849
|
+
simulcastSettings = this.defaultSimulcastSettings,
|
|
1850
|
+
enableDtx = false
|
|
1851
|
+
) {
|
|
1852
|
+
|
|
1853
|
+
this.isSupposeToBeConnected = true;
|
|
1854
|
+
|
|
1855
|
+
if (this.isConnecting) {
|
|
1856
|
+
this.emit('error', {type: 'warning', id: 23, message: 'connection already in progress'});
|
|
1857
|
+
return
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if(this.isConnected) {
|
|
1861
|
+
await this.disconnect();
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
this._abortController = new AbortController();
|
|
1865
|
+
|
|
1866
|
+
this.sessionId = null;
|
|
1867
|
+
this.server = server;
|
|
1868
|
+
this.protocol = protocol;
|
|
1869
|
+
this.iceServers = iceServers;
|
|
1870
|
+
this.token = token;
|
|
1871
|
+
this.roomId = roomId;
|
|
1872
|
+
this.pin = pin;
|
|
1873
|
+
this.id = null;
|
|
1874
|
+
this.display = display;
|
|
1875
|
+
this.userId = userId;
|
|
1876
|
+
this.webrtcVersion = webrtcVersion;
|
|
1877
|
+
this.initialBitrate = initialBitrate;
|
|
1878
|
+
this.recordingFilename = recordingFilename;
|
|
1879
|
+
this.enableDtx = enableDtx;
|
|
1880
|
+
this.simulcast = simulcast;
|
|
1881
|
+
this.simulcastSettings = structuredClone(simulcastSettings);
|
|
1882
|
+
this.useWebsockets = this.protocol === 'ws' || this.protocol === 'wss';
|
|
1883
|
+
|
|
1884
|
+
// fixing wrong order
|
|
1885
|
+
Object.keys(this.simulcastSettings).forEach(source => {
|
|
1886
|
+
const wrongOrder = this.simulcastSettings[source].bitrates[0].rid === 'l';
|
|
1887
|
+
if(wrongOrder) {
|
|
1888
|
+
this.simulcastSettings[source].bitrates = this.simulcastSettings[source].bitrates.reverse();
|
|
1889
|
+
this.simulcastSettings[source].defaultSubstream = 2 - this.simulcastSettings[source].defaultSubstream
|
|
1890
|
+
}
|
|
1891
|
+
})
|
|
1892
|
+
|
|
1893
|
+
this.isConnecting = true;
|
|
1894
|
+
this.emit('joining', true);
|
|
1895
|
+
|
|
1896
|
+
try {
|
|
1897
|
+
|
|
1898
|
+
if(this.useWebsockets) {
|
|
1899
|
+
await this.#webSocketConnection(false);
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
await this.#httpConnection(false);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
this.#publisherHandle = await this.#createHandle();
|
|
1906
|
+
this.handleId = this.#publisherHandle.handleId;
|
|
1907
|
+
|
|
1908
|
+
this.#subscriberHandle = await this.#createHandle();
|
|
1909
|
+
|
|
1910
|
+
await this.#joinAsPublisher(this.roomId, this.pin, this.userId, this.display);
|
|
1911
|
+
await this.#waitForConnectEvent();
|
|
1912
|
+
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
this.emit('error', error);
|
|
1915
|
+
} finally {
|
|
1916
|
+
this.isConnecting = false;
|
|
1917
|
+
this.emit('joining', false);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
this.#enableStatsWatch();
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async disconnect() {
|
|
1924
|
+
|
|
1925
|
+
this.isSupposeToBeConnected = false;
|
|
1926
|
+
|
|
1927
|
+
if (this.isDisconnecting) {
|
|
1928
|
+
return
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
this.#disableStatsWatch();
|
|
1932
|
+
|
|
1933
|
+
this._abortController?.abort?.();
|
|
1934
|
+
this.isDisconnecting = true;
|
|
1935
|
+
this.#stopKeepAlive();
|
|
1936
|
+
let isConnected = this.isConnected;
|
|
1937
|
+
|
|
1938
|
+
if(this.#publisherHandle) {
|
|
1939
|
+
this.#emitLocalFeedUpdate({feedRemoval: true});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
if(this.#subscriberHandle) {
|
|
1943
|
+
[...this.#getCachedAttendeeIds()].forEach(id => {
|
|
1944
|
+
this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id}});
|
|
1945
|
+
this.#removeAttendeeFromCache(id);
|
|
1946
|
+
})
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
await this.#destroyHandle(this.#publisherHandle?.handleId, false);
|
|
1950
|
+
await this.#destroyHandle(this.#subscriberHandle?.handleId, false);
|
|
1951
|
+
|
|
1952
|
+
this.#wipeListeners();
|
|
1953
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
1954
|
+
await this.#send({"janus": "destroy"}, true);
|
|
1955
|
+
this.ws.close();
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
this.#subscriberJoinPromise = null;
|
|
1959
|
+
this.sessionId = null;
|
|
1960
|
+
this.isPublished = false;
|
|
1961
|
+
this.isConnected = false;
|
|
1962
|
+
this.isDisconnecting = false;
|
|
1963
|
+
this.emit('publishing', false);
|
|
1964
|
+
this.emit('published', false);
|
|
1965
|
+
this.emit('joining', false);
|
|
1966
|
+
this.emit('joined', false);
|
|
1967
|
+
this.emit('disconnect', isConnected);
|
|
1968
|
+
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
async destroy() {
|
|
1972
|
+
return this.disconnect()
|
|
1973
|
+
.then(() => {
|
|
1974
|
+
this.clear();
|
|
1975
|
+
return true;
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
#enableDebug() {
|
|
1980
|
+
this._log = console.log.bind(console);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
#getHandle(handleId, rfid = null, userId = null, fullUserId = null) {
|
|
1984
|
+
|
|
1985
|
+
if(handleId === this.#subscriberHandle?.handleId) {
|
|
1986
|
+
return this.#subscriberHandle;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if(handleId === this.#publisherHandle?.handleId) {
|
|
1990
|
+
return this.#publisherHandle;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
#findSimulcastConfig(source, settings) {
|
|
1998
|
+
return Object.keys(settings).reduce((acc, key) => {
|
|
1999
|
+
if(settings[source]) {
|
|
2000
|
+
return settings[source];
|
|
2001
|
+
} else if(source.indexOf(key.match(/\*(.*?)\*/)?.[1]) > -1) {
|
|
2002
|
+
return settings[key];
|
|
2003
|
+
} else return acc;
|
|
2004
|
+
}, settings['default']);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
#sendTrickleCandidate(handleId, candidate) {
|
|
2008
|
+
return this.#send({
|
|
2009
|
+
"janus": "trickle",
|
|
2010
|
+
"candidate": candidate,
|
|
2011
|
+
"handle_id": handleId
|
|
2012
|
+
}, false, false, 5)
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
#webrtc(handleId, enableOntrack = false, enableOnnegotiationneeded = false) {
|
|
2016
|
+
|
|
2017
|
+
let handle = this.#getHandle(handleId);
|
|
2018
|
+
if (!handle) {
|
|
2019
|
+
this.emit('error', {
|
|
2020
|
+
type: 'error',
|
|
2021
|
+
id: 24,
|
|
2022
|
+
message: 'id non-existent',
|
|
2023
|
+
data: [handleId, 'create rtc connection']
|
|
2024
|
+
});
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
let config = handle.webrtcStuff;
|
|
2029
|
+
if (!config.pc) {
|
|
2030
|
+
let pc_config = {"iceServers": this.iceServers, "iceTransportPolicy": 'all', "bundlePolicy": undefined};
|
|
2031
|
+
|
|
2032
|
+
pc_config["sdpSemantics"] = "unified-plan";
|
|
2033
|
+
|
|
2034
|
+
let pc_constraints = {};
|
|
2035
|
+
|
|
2036
|
+
if (adapter.browserDetails.browser === "edge") {
|
|
2037
|
+
// This is Edge, enable BUNDLE explicitly
|
|
2038
|
+
pc_config.bundlePolicy = "max-bundle";
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// pc_config.bundlePolicy = 'balanced';
|
|
2042
|
+
// pc_config.iceTransportPolicy = 'relay';
|
|
2043
|
+
// pc_config.rtcpMuxPolicy = "negotiate";
|
|
2044
|
+
|
|
2045
|
+
this._log('new RTCPeerConnection', pc_config, pc_constraints);
|
|
2046
|
+
|
|
2047
|
+
config.pc = new RTCPeerConnection(pc_config, pc_constraints);
|
|
2048
|
+
|
|
2049
|
+
if(enableOnnegotiationneeded) {
|
|
2050
|
+
config.pc.onnegotiationneeded = async () => {
|
|
2051
|
+
try {
|
|
2052
|
+
this._log('Starting negotiation from onnegotiationneeded');
|
|
2053
|
+
|
|
2054
|
+
// Create an offer
|
|
2055
|
+
const jsep = await this.#createAO('offer', handle.handleId, false);
|
|
2056
|
+
|
|
2057
|
+
if (!jsep) {
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
//HOTFIX: Temporary fix for Safari 13
|
|
2062
|
+
if (jsep.sdp && jsep.sdp.indexOf("\r\na=ice-ufrag") === -1) {
|
|
2063
|
+
jsep.sdp = jsep.sdp.replace("\na=ice-ufrag", "\r\na=ice-ufrag");
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
let descriptions = [];
|
|
2067
|
+
let transceivers = config.pc.getTransceivers();
|
|
2068
|
+
Object.keys(config.streamMap[this.id]).forEach(source => {
|
|
2069
|
+
const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
|
|
2070
|
+
config.streamMap[this.id][source].forEach(trackId => {
|
|
2071
|
+
let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
|
|
2072
|
+
if(t) {
|
|
2073
|
+
descriptions.push({mid: t.mid, description: JSON.stringify({source, simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: this._talkIntercomChannels})});
|
|
2074
|
+
}
|
|
2075
|
+
})
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// Send the offer to the server
|
|
2079
|
+
await this.sendMessage(handle.handleId, {
|
|
2080
|
+
body: {
|
|
2081
|
+
"request": "configure",
|
|
2082
|
+
"audio": !!(config.stream && config.stream.getAudioTracks().length > 0),
|
|
2083
|
+
"video": !!(config.stream && config.stream.getVideoTracks().length > 0),
|
|
2084
|
+
"data": true,
|
|
2085
|
+
descriptions: descriptions,
|
|
2086
|
+
...(this.recordingFilename ? {filename: this.recordingFilename} : {}),
|
|
2087
|
+
},
|
|
2088
|
+
jsep
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
// The answer will be processed by #handleWsEvents, so we don't need to do anything else here
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
this._log('Error during negotiation:', error);
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
config.pc.onconnectionstatechange = () => {
|
|
2099
|
+
|
|
2100
|
+
if (config.pc.connectionState === 'failed') {
|
|
2101
|
+
this._log('onconnectionstatechange: connectionState === failed');
|
|
2102
|
+
this.#iceRestart(handleId);
|
|
2103
|
+
}
|
|
2104
|
+
this.emit('connectionState', [handleId, handleId === this.#publisherHandle?.handleId, config.pc.connectionState]);
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
config.pc.oniceconnectionstatechange = () => {
|
|
2108
|
+
if (config.pc.iceConnectionState === 'failed') {
|
|
2109
|
+
this._log('oniceconnectionstatechange: iceConnectionState === failed');
|
|
2110
|
+
this.#iceRestart(handleId);
|
|
2111
|
+
}
|
|
2112
|
+
this.emit('iceState', [handleId, handleId === this.#publisherHandle?.handleId, config.pc.iceConnectionState]);
|
|
2113
|
+
};
|
|
2114
|
+
config.pc.onicecandidate = (event) => {
|
|
2115
|
+
if (event.candidate == null || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
|
|
2116
|
+
config.iceDone = true;
|
|
2117
|
+
this.#sendTrickleCandidate(handleId, {"completed": true})
|
|
2118
|
+
.catch(e => {
|
|
2119
|
+
this.emit('error', e);
|
|
2120
|
+
});
|
|
2121
|
+
} else {
|
|
2122
|
+
// JSON.stringify doesn't work on some WebRTC objects anymore
|
|
2123
|
+
// See https://code.google.com/p/chromium/issues/detail?id=467366
|
|
2124
|
+
var candidate = {
|
|
2125
|
+
"candidate": event.candidate.candidate,
|
|
2126
|
+
"sdpMid": event.candidate.sdpMid,
|
|
2127
|
+
"sdpMLineIndex": event.candidate.sdpMLineIndex
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
this.#sendTrickleCandidate(handleId, candidate)
|
|
2131
|
+
.catch(e => {
|
|
2132
|
+
this.emit('error', e);
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
if (enableOntrack) {
|
|
2138
|
+
config.pc.ontrack = (event) => {
|
|
2139
|
+
|
|
2140
|
+
if(!event.streams)
|
|
2141
|
+
return;
|
|
2142
|
+
|
|
2143
|
+
if(!event.streams?.[0]?.onremovetrack) {
|
|
2144
|
+
|
|
2145
|
+
event.streams[0].onremovetrack = (ev) => {
|
|
2146
|
+
|
|
2147
|
+
this._log('Remote track removed', ev);
|
|
2148
|
+
|
|
2149
|
+
const trackIndex = config?.tracks?.findIndex(t => t.id === ev.track.id);
|
|
2150
|
+
if(trackIndex > -1) {
|
|
2151
|
+
config.tracks.splice(trackIndex, 1);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// check if handle still exists
|
|
2155
|
+
if(!this.#getHandle(handle.handleId)) {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
this.#updatePublishersStreamMap();
|
|
2160
|
+
|
|
2161
|
+
let transceiver = config.pc?.getTransceivers()?.find(
|
|
2162
|
+
t => t.receiver.track === ev.track);
|
|
2163
|
+
|
|
2164
|
+
let mid = transceiver?.mid || ev.track.id;
|
|
2165
|
+
|
|
2166
|
+
// transceiver may not exist already
|
|
2167
|
+
if(mid === ev.target.id && config.mids?.[event.track.id]) {
|
|
2168
|
+
mid = config.mids[event.track.id];
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
this.#emitRemoteFeedUpdate(mid, {
|
|
2172
|
+
removingTrack: true,
|
|
2173
|
+
track: ev.track,
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
delete config.mids[ev.track.id];
|
|
2177
|
+
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
if (event.track) {
|
|
2182
|
+
|
|
2183
|
+
this._log('Remote track added', event.track, event.streams[0]);
|
|
2184
|
+
|
|
2185
|
+
config.tracks.push(event.track);
|
|
2186
|
+
|
|
2187
|
+
this.#updatePublishersStreamMap();
|
|
2188
|
+
|
|
2189
|
+
let mid = event.transceiver ? event.transceiver.mid : event.track.id;
|
|
2190
|
+
config.mids[event.track.id] = event.transceiver.mid
|
|
2191
|
+
|
|
2192
|
+
if(event.track.kind === 'video') {
|
|
2193
|
+
this.requestKeyFrame(handle.handleId, mid);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
this.#emitRemoteFeedUpdate(mid, {
|
|
2197
|
+
addingTrack: true,
|
|
2198
|
+
track: event.track
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
if (event.track.onended)
|
|
2202
|
+
return;
|
|
2203
|
+
|
|
2204
|
+
event.track.onended = (ev) => {
|
|
2205
|
+
this._log('Remote track ended', ev);
|
|
2206
|
+
|
|
2207
|
+
const trackIndex = config?.tracks?.findIndex(t => t.id === ev.target.id);
|
|
2208
|
+
|
|
2209
|
+
if(trackIndex > -1) {
|
|
2210
|
+
config.tracks.splice(trackIndex, 1);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// check if handle still exists
|
|
2214
|
+
if(!this.#getHandle(handle.handleId)) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
this.#updatePublishersStreamMap();
|
|
2219
|
+
|
|
2220
|
+
let transceiver = config.pc?.getTransceivers()?.find(t => t.receiver.track === ev.target);
|
|
2221
|
+
let mid = transceiver?.mid || ev.target.id;
|
|
2222
|
+
|
|
2223
|
+
// transceiver may not exist already
|
|
2224
|
+
if(mid === ev.target.id && config.mids?.[event.track.id]) {
|
|
2225
|
+
mid = config.mids[event.track.id];
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
this.#emitRemoteFeedUpdate(mid, {
|
|
2229
|
+
removingTrack: true,
|
|
2230
|
+
track: ev.target
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
delete config.mids[event.track.id];
|
|
2234
|
+
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
event.track.onmute = (ev) => {
|
|
2238
|
+
this._log('Remote track muted');
|
|
2239
|
+
|
|
2240
|
+
let transceiver = config.pc.getTransceivers().find(
|
|
2241
|
+
t => t.receiver.track === ev.target);
|
|
2242
|
+
let mid = transceiver.mid || ev.target.id;
|
|
2243
|
+
|
|
2244
|
+
this.#emitRemoteTrackMuted(mid, {track:ev.target, muted: true});
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
event.track.onunmute = (ev) => {
|
|
2248
|
+
this._log('Remote track unmuted');
|
|
2249
|
+
|
|
2250
|
+
let transceiver = config.pc.getTransceivers().find(
|
|
2251
|
+
t => t.receiver.track === ev.target);
|
|
2252
|
+
let mid = transceiver.mid || ev.target.id;
|
|
2253
|
+
|
|
2254
|
+
this.#emitRemoteTrackMuted(mid, {track:ev.target, muted: false});
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if (!config.dataChannel || !config.dataChannelOpen) {
|
|
2262
|
+
|
|
2263
|
+
config.dataChannel = {};
|
|
2264
|
+
|
|
2265
|
+
var onDataChannelMessage = (event) => {
|
|
2266
|
+
this.#handleDataChannelEvents(handleId, 'message', event.data);
|
|
2267
|
+
};
|
|
2268
|
+
var onDataChannelStateChange = (event) => {
|
|
2269
|
+
let label = event.target.label;
|
|
2270
|
+
let protocol = event.target.protocol;
|
|
2271
|
+
let state = config.dataChannel[label] ? config.dataChannel[label].readyState : "null";
|
|
2272
|
+
this.#handleDataChannelEvents(handleId, 'state', {state, label} );
|
|
2273
|
+
};
|
|
2274
|
+
var onDataChannelError = (error) => {
|
|
2275
|
+
this.#handleDataChannelEvents(handleId, 'error', {label: error?.channel?.label, error});
|
|
2276
|
+
};
|
|
2277
|
+
|
|
2278
|
+
const createDataChannel = (label, protocol, incoming) => {
|
|
2279
|
+
let options = {ordered: true};
|
|
2280
|
+
if(!incoming) {
|
|
2281
|
+
if(protocol) {
|
|
2282
|
+
options = {...options, protocol}
|
|
2283
|
+
}
|
|
2284
|
+
config.dataChannel[label] = config.pc.createDataChannel(label, options);
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
config.dataChannel[label] = incoming;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
config.dataChannel[label].onmessage = onDataChannelMessage;
|
|
2291
|
+
config.dataChannel[label].onopen = onDataChannelStateChange;
|
|
2292
|
+
config.dataChannel[label].onclose = onDataChannelStateChange;
|
|
2293
|
+
config.dataChannel[label].onerror = onDataChannelError;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
createDataChannel(this.defaultDataChannelLabel, null, null);
|
|
2297
|
+
|
|
2298
|
+
config.pc.ondatachannel = function (event) {
|
|
2299
|
+
createDataChannel(event.channel.label, event.channel.protocol, event.channel)
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
#webrtcPeer(handleId, jsep) {
|
|
2305
|
+
|
|
2306
|
+
let handle = this.#getHandle(handleId);
|
|
2307
|
+
if (!handle) {
|
|
2308
|
+
return Promise.reject({type: 'warning', id: 25, message: 'id non-existent', data: [handleId, 'rtc peer']});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
var config = handle.webrtcStuff;
|
|
2312
|
+
|
|
2313
|
+
if (jsep !== undefined && jsep !== null) {
|
|
2314
|
+
if (config.pc === null) {
|
|
2315
|
+
this._log("No PeerConnection: if this is an answer, use createAnswer and not #webrtcPeer");
|
|
2316
|
+
return Promise.resolve(null);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
return config.pc.setRemoteDescription(jsep)
|
|
2320
|
+
.then(() => {
|
|
2321
|
+
config.remoteSdp = jsep.sdp;
|
|
2322
|
+
// Any trickle candidate we cached?
|
|
2323
|
+
if (config.candidates && config.candidates.length > 0) {
|
|
2324
|
+
for (var i = 0; i < config.candidates.length; i++) {
|
|
2325
|
+
var candidate = config.candidates[i];
|
|
2326
|
+
if (!candidate || candidate.completed === true) {
|
|
2327
|
+
config.pc.addIceCandidate(null).catch((e) => {
|
|
2328
|
+
this._log('Error adding null candidate', e);
|
|
2329
|
+
});;
|
|
2330
|
+
} else {
|
|
2331
|
+
config.pc.addIceCandidate(candidate).catch((e) => {
|
|
2332
|
+
this._log('Error adding candidate', e);
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
config.candidates = [];
|
|
2337
|
+
}
|
|
2338
|
+
// Done
|
|
2339
|
+
return true;
|
|
2340
|
+
})
|
|
2341
|
+
.catch((e) => {
|
|
2342
|
+
return Promise.reject({type: 'warning', id: 26, message: 'rtc peer', data: [handleId, e]});
|
|
2343
|
+
});
|
|
2344
|
+
} else {
|
|
2345
|
+
return Promise.reject({type: 'warning', id: 27, message: 'rtc peer', data: [handleId, 'invalid jsep']});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
#iceRestart(handleId) {
|
|
2350
|
+
|
|
2351
|
+
let handle = this.#getHandle(handleId);
|
|
2352
|
+
if (!handle) {
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
var config = handle.webrtcStuff;
|
|
2356
|
+
|
|
2357
|
+
// Already restarting;
|
|
2358
|
+
if (config.isIceRestarting) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
config.isIceRestarting = true;
|
|
2363
|
+
|
|
2364
|
+
// removing this so we can cache ice candidates again
|
|
2365
|
+
config.remoteSdp = null;
|
|
2366
|
+
|
|
2367
|
+
if (this.#publisherHandle.handleId === handleId) {
|
|
2368
|
+
this._log('Performing local ICE restart');
|
|
2369
|
+
let hasAudio = !!(config.stream && config.stream.getAudioTracks().length > 0);
|
|
2370
|
+
let hasVideo = !!(config.stream && config.stream.getVideoTracks().length > 0);
|
|
2371
|
+
this.#createAO('offer', handleId, true )
|
|
2372
|
+
.then((jsep) => {
|
|
2373
|
+
if (!jsep) {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
return this.sendMessage(handleId, {
|
|
2377
|
+
body: {"request": "configure", "keyframe": true, "audio": hasAudio, "video": hasVideo, "data": true, ...(this.recordingFilename ? {filename: this.recordingFilename} : {})},
|
|
2378
|
+
jsep
|
|
2379
|
+
}, false, false, 5);
|
|
2380
|
+
})
|
|
2381
|
+
.then(r => {
|
|
2382
|
+
config.isIceRestarting = false;
|
|
2383
|
+
this._log('ICE restart success');
|
|
2384
|
+
})
|
|
2385
|
+
.catch((e) => {
|
|
2386
|
+
config.isIceRestarting = false;
|
|
2387
|
+
this.emit('error', {type: 'error', id: 28, message: 'iceRestart failed', data: e});
|
|
2388
|
+
});
|
|
2389
|
+
} else {
|
|
2390
|
+
this._log('Performing remote ICE restart', handleId);
|
|
2391
|
+
return this.sendMessage(handleId, {
|
|
2392
|
+
body: {"request": "configure", "restart": true}
|
|
2393
|
+
}, false, false, 5).then(() => {
|
|
2394
|
+
}).then(() => {
|
|
2395
|
+
config.isIceRestarting = false;
|
|
2396
|
+
this._log('ICE restart success');
|
|
2397
|
+
}).catch(() => {
|
|
2398
|
+
config.isIceRestarting = false;
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
#setupTransceivers(handleId, [audioSend, audioRecv, videoSend, videoRecv, audioTransceiver = null, videoTransceiver = null]) {
|
|
2404
|
+
//TODO: this should be refactored to use handle's trackMap so we dont have to pass any parameters
|
|
2405
|
+
|
|
2406
|
+
let handle = this.#getHandle(handleId);
|
|
2407
|
+
if (!handle) {
|
|
2408
|
+
return null;
|
|
2409
|
+
}
|
|
2410
|
+
let config = handle.webrtcStuff;
|
|
2411
|
+
|
|
2412
|
+
const setTransceiver = (transceiver, send, recv, kind = 'audio') => {
|
|
2413
|
+
if (!send && !recv) {
|
|
2414
|
+
// disabled: have we removed it?
|
|
2415
|
+
if (transceiver) {
|
|
2416
|
+
if (transceiver.setDirection) {
|
|
2417
|
+
transceiver.setDirection("inactive");
|
|
2418
|
+
} else {
|
|
2419
|
+
transceiver.direction = "inactive";
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
} else {
|
|
2423
|
+
if (send && recv) {
|
|
2424
|
+
if (transceiver) {
|
|
2425
|
+
if (transceiver.setDirection) {
|
|
2426
|
+
transceiver.setDirection("sendrecv");
|
|
2427
|
+
} else {
|
|
2428
|
+
transceiver.direction = "sendrecv";
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
} else if (send && !recv) {
|
|
2432
|
+
if (transceiver) {
|
|
2433
|
+
if (transceiver.setDirection) {
|
|
2434
|
+
transceiver.setDirection("sendonly");
|
|
2435
|
+
} else {
|
|
2436
|
+
transceiver.direction = "sendonly";
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
} else if (!send && recv) {
|
|
2440
|
+
if (transceiver) {
|
|
2441
|
+
if (transceiver.setDirection) {
|
|
2442
|
+
transceiver.setDirection("recvonly");
|
|
2443
|
+
} else {
|
|
2444
|
+
transceiver.direction = "recvonly";
|
|
2445
|
+
}
|
|
2446
|
+
} else {
|
|
2447
|
+
// In theory, this is the only case where we might not have a transceiver yet
|
|
2448
|
+
config.pc.addTransceiver(kind, {direction: "recvonly"});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// if we're passing any transceivers, we work only on them, doesn't matter if one of them is null
|
|
2455
|
+
if(audioTransceiver || videoTransceiver) {
|
|
2456
|
+
if(audioTransceiver) {
|
|
2457
|
+
setTransceiver(audioTransceiver, audioSend, audioRecv, 'audio');
|
|
2458
|
+
}
|
|
2459
|
+
if(videoTransceiver) {
|
|
2460
|
+
setTransceiver(videoTransceiver, videoSend, videoRecv, 'video');
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
// else we work on all transceivers
|
|
2464
|
+
else {
|
|
2465
|
+
let transceivers = config.pc.getTransceivers();
|
|
2466
|
+
if (transceivers && transceivers.length > 0) {
|
|
2467
|
+
for (let i in transceivers) {
|
|
2468
|
+
let t = transceivers[i];
|
|
2469
|
+
if (
|
|
2470
|
+
(t.sender && t.sender.track && t.sender.track.kind === "audio") ||
|
|
2471
|
+
(t.receiver && t.receiver.track && t.receiver.track.kind === "audio")
|
|
2472
|
+
) {
|
|
2473
|
+
setTransceiver(t, audioSend, audioRecv, 'audio');
|
|
2474
|
+
}
|
|
2475
|
+
if (
|
|
2476
|
+
(t.sender && t.sender.track && t.sender.track.kind === "video") ||
|
|
2477
|
+
(t.receiver && t.receiver.track && t.receiver.track.kind === "video")
|
|
2478
|
+
) {
|
|
2479
|
+
setTransceiver(t, videoSend, videoRecv, 'video');
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
#createAO(type = 'offer', handleId, iceRestart = false, ) {
|
|
2487
|
+
|
|
2488
|
+
let handle = this.#getHandle(handleId);
|
|
2489
|
+
if (!handle) {
|
|
2490
|
+
return Promise.reject({
|
|
2491
|
+
type: 'warning',
|
|
2492
|
+
id: 29,
|
|
2493
|
+
message: 'id non-existent',
|
|
2494
|
+
data: [handleId, 'createAO', type]
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
let methodName = null;
|
|
2499
|
+
if (type === 'offer') {
|
|
2500
|
+
methodName = 'createOffer'
|
|
2501
|
+
} else {
|
|
2502
|
+
methodName = 'createAnswer'
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
let config = handle.webrtcStuff;
|
|
2506
|
+
let mediaConstraints = {};
|
|
2507
|
+
|
|
2508
|
+
if (iceRestart) {
|
|
2509
|
+
mediaConstraints["iceRestart"] = true;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
return config.pc[methodName](mediaConstraints)
|
|
2513
|
+
.then( (response) => {
|
|
2514
|
+
|
|
2515
|
+
// if type offer and its me and we want dtx we mungle the sdp
|
|
2516
|
+
if(handleId === this.#publisherHandle.handleId && type === 'offer' && this.enableDtx) {
|
|
2517
|
+
// enable DTX
|
|
2518
|
+
response.sdp = response.sdp.replace("useinbandfec=1", "useinbandfec=1;usedtx=1")
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
config.mySdp = response.sdp;
|
|
2522
|
+
let _p = config.pc.setLocalDescription(response)
|
|
2523
|
+
.catch((e) => {
|
|
2524
|
+
return Promise.reject({
|
|
2525
|
+
type: 'warning',
|
|
2526
|
+
id: 30,
|
|
2527
|
+
message: 'setLocalDescription',
|
|
2528
|
+
data: [handleId, e]
|
|
2529
|
+
})
|
|
2530
|
+
});
|
|
2531
|
+
config.mediaConstraints = mediaConstraints;
|
|
2532
|
+
if (!config.iceDone && !config.trickle) {
|
|
2533
|
+
// Don't do anything until we have all candidates
|
|
2534
|
+
return Promise.resolve(null);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// JSON.stringify doesn't work on some WebRTC objects anymore
|
|
2538
|
+
// See https://code.google.com/p/chromium/issues/detail?id=467366
|
|
2539
|
+
var jsep = {
|
|
2540
|
+
"type": response.type,
|
|
2541
|
+
"sdp": response.sdp
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
if(response.e2ee)
|
|
2545
|
+
jsep.e2ee = true;
|
|
2546
|
+
if(response.rid_order === "hml" || response.rid_order === "lmh")
|
|
2547
|
+
jsep.rid_order = response.rid_order;
|
|
2548
|
+
if(response.force_relay)
|
|
2549
|
+
jsep.force_relay = true;
|
|
2550
|
+
|
|
2551
|
+
return _p.then(() => jsep)
|
|
2552
|
+
}, (e) => {
|
|
2553
|
+
return Promise.reject({type: 'warning', id: 31, message: methodName, data: [handleId, e]})
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
#publishRemote(handleId, jsep) {
|
|
2559
|
+
let handle = this.#getHandle(handleId);
|
|
2560
|
+
if (!handle) {
|
|
2561
|
+
return Promise.reject({
|
|
2562
|
+
type: 'warning',
|
|
2563
|
+
id: 32,
|
|
2564
|
+
message: 'id non-existent',
|
|
2565
|
+
data: [handleId, 'publish remote participant']
|
|
2566
|
+
})
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
this.#webrtc(handleId, true, false);
|
|
2570
|
+
|
|
2571
|
+
let config = handle.webrtcStuff;
|
|
2572
|
+
|
|
2573
|
+
if (jsep) {
|
|
2574
|
+
return config.pc.setRemoteDescription(jsep)
|
|
2575
|
+
.then(() => {
|
|
2576
|
+
config.remoteSdp = jsep.sdp;
|
|
2577
|
+
// Any trickle candidate we cached?
|
|
2578
|
+
if (config.candidates && config.candidates.length > 0) {
|
|
2579
|
+
for (var i = 0; i < config.candidates.length; i++) {
|
|
2580
|
+
var candidate = config.candidates[i];
|
|
2581
|
+
if (!candidate || candidate.completed === true) {
|
|
2582
|
+
// end-of-candidates
|
|
2583
|
+
config.pc.addIceCandidate(null).catch(e => {
|
|
2584
|
+
this._log('Error adding null candidate', e);
|
|
2585
|
+
});
|
|
2586
|
+
} else {
|
|
2587
|
+
// New candidate
|
|
2588
|
+
config.pc.addIceCandidate(candidate).catch(e => {
|
|
2589
|
+
this._log('Error adding candidate', e);
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
config.candidates = [];
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
this.#setupTransceivers(handleId, [false, true, false, true]);
|
|
2597
|
+
|
|
2598
|
+
// Create the answer now
|
|
2599
|
+
return this.#createAO('answer', handleId, false)
|
|
2600
|
+
.then(_jsep => {
|
|
2601
|
+
if (!_jsep) {
|
|
2602
|
+
this.emit('error', {
|
|
2603
|
+
type: 'warning',
|
|
2604
|
+
id: 33,
|
|
2605
|
+
message: 'publish remote participant',
|
|
2606
|
+
data: [handleId, 'no jsep']
|
|
2607
|
+
});
|
|
2608
|
+
return Promise.resolve();
|
|
2609
|
+
}
|
|
2610
|
+
return this.sendMessage(handleId, {
|
|
2611
|
+
"body": {"request": "start", ...(this.roomId && {"room": this.roomId}), ...(this.pin && {pin: this.pin})},
|
|
2612
|
+
"jsep": _jsep
|
|
2613
|
+
});
|
|
2614
|
+
})
|
|
2615
|
+
}, (e) => Promise.reject({
|
|
2616
|
+
type: 'warning',
|
|
2617
|
+
id: 34,
|
|
2618
|
+
message: 'setRemoteDescription',
|
|
2619
|
+
data: [handleId, e]
|
|
2620
|
+
}));
|
|
2621
|
+
|
|
2622
|
+
} else {
|
|
2623
|
+
return Promise.resolve();
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
#republishOnTrackEnded(source) {
|
|
2629
|
+
let handle = this.#publisherHandle;
|
|
2630
|
+
if (!handle) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
let config = handle.webrtcStuff;
|
|
2634
|
+
if (!config.stream) {
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
let sourceTrackIds = (config.streamMap[this.id][source] || []);
|
|
2638
|
+
let remainingTracks = [];
|
|
2639
|
+
for(let i = 0; i < sourceTrackIds.length; i++) {
|
|
2640
|
+
let foundTrack = config.tracks.find(t => t.id === sourceTrackIds[i]);
|
|
2641
|
+
if(foundTrack) {
|
|
2642
|
+
remainingTracks.push(foundTrack);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
if (remainingTracks.length) {
|
|
2646
|
+
let stream = new MediaStream();
|
|
2647
|
+
remainingTracks.forEach(track => stream.addTrack(track));
|
|
2648
|
+
return this.publishLocal(stream, source);
|
|
2649
|
+
}
|
|
2650
|
+
else {
|
|
2651
|
+
return this.publishLocal(null, source);
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2655
|
+
async publishLocal(stream = null, source = 'camera0') {
|
|
2656
|
+
|
|
2657
|
+
if(!this.isConnected || this.isDisconnecting) {
|
|
2658
|
+
return {
|
|
2659
|
+
type: 'warning',
|
|
2660
|
+
id: 35,
|
|
2661
|
+
message: 'Either not connected or disconnecting',
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
if(stream?.getVideoTracks()?.length > 1) {
|
|
2666
|
+
return {
|
|
2667
|
+
type: 'warning',
|
|
2668
|
+
id: 36,
|
|
2669
|
+
message: 'multiple video tracks not supported',
|
|
2670
|
+
data: null
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
if(stream?.getAudioTracks()?.length > 1) {
|
|
2675
|
+
return {
|
|
2676
|
+
type: 'warning',
|
|
2677
|
+
id: 37,
|
|
2678
|
+
message: 'multiple audio tracks not supported',
|
|
2679
|
+
data: null
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
let handle = this.#publisherHandle;
|
|
2684
|
+
if (!handle) {
|
|
2685
|
+
return {
|
|
2686
|
+
type: 'error',
|
|
2687
|
+
id: 38,
|
|
2688
|
+
message: 'no local handle, connect before publishing',
|
|
2689
|
+
data: null
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
this.emit('publishing', true);
|
|
2694
|
+
|
|
2695
|
+
this.#webrtc(handle.handleId, false, true);
|
|
2696
|
+
|
|
2697
|
+
let config = handle.webrtcStuff;
|
|
2698
|
+
|
|
2699
|
+
if (!config.stream) {
|
|
2700
|
+
config.stream = new MediaStream();
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
let transceivers = config.pc.getTransceivers();
|
|
2704
|
+
let existingTracks = [...(config.streamMap?.[this.id]?.[source] || [])];
|
|
2705
|
+
|
|
2706
|
+
if(stream?.getTracks().length) {
|
|
2707
|
+
if(!config.streamMap[this.id]) {
|
|
2708
|
+
config.streamMap[this.id] = {};
|
|
2709
|
+
}
|
|
2710
|
+
config.streamMap[this.id][source] = stream?.getTracks()?.map(track => track.id) || [];
|
|
2711
|
+
} else {
|
|
2712
|
+
if(config.streamMap[this.id]) {
|
|
2713
|
+
delete config.streamMap[this.id][source];
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// remove old audio track related to this source
|
|
2718
|
+
let oldAudioStreamIndex = config?.tracks?.findIndex(track => track.kind === 'audio' && existingTracks.includes(track.id));
|
|
2719
|
+
let oldAudioStream = config?.tracks[oldAudioStreamIndex];
|
|
2720
|
+
if (oldAudioStream) {
|
|
2721
|
+
try {
|
|
2722
|
+
oldAudioStream.stop();
|
|
2723
|
+
|
|
2724
|
+
} catch (e) {
|
|
2725
|
+
this._log(e);
|
|
2726
|
+
}
|
|
2727
|
+
config.stream.removeTrack(oldAudioStream);
|
|
2728
|
+
config?.tracks.splice(oldAudioStreamIndex, 1);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// remove old video track related to this source
|
|
2732
|
+
let oldVideoStreamIndex = config?.tracks?.findIndex(track => track.kind === 'video' && existingTracks.includes(track.id));
|
|
2733
|
+
let oldVideoStream = config?.tracks[oldVideoStreamIndex];
|
|
2734
|
+
if (oldVideoStream) {
|
|
2735
|
+
try {
|
|
2736
|
+
oldVideoStream.stop();
|
|
2737
|
+
} catch (e) {
|
|
2738
|
+
this._log(e);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
config.stream.removeTrack(oldVideoStream);
|
|
2742
|
+
config?.tracks.splice(oldVideoStreamIndex, 1);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
|
|
2746
|
+
let audioTrackReplacePromise = Promise.resolve();
|
|
2747
|
+
let videoTrackReplacePromise = Promise.resolve();
|
|
2748
|
+
|
|
2749
|
+
let audioTransceiver = null;
|
|
2750
|
+
let videoTransceiver = null;
|
|
2751
|
+
let replaceAudio = stream?.getAudioTracks()?.length;
|
|
2752
|
+
let replaceVideo = stream?.getVideoTracks()?.length;
|
|
2753
|
+
|
|
2754
|
+
for(const transceiver of transceivers) {
|
|
2755
|
+
if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'audio' && existingTracks.includes(transceiver.sender?.track?.id)) {
|
|
2756
|
+
audioTransceiver = transceiver;
|
|
2757
|
+
}
|
|
2758
|
+
else if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'video' && existingTracks.includes(transceiver.sender?.track?.id)) {
|
|
2759
|
+
videoTransceiver = transceiver;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Reusing existing transceivers
|
|
2763
|
+
// TODO: if we start using different codecs for different sources, we need to check for that here
|
|
2764
|
+
|
|
2765
|
+
else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('audio') > -1) && replaceAudio && !audioTransceiver) {
|
|
2766
|
+
audioTransceiver = transceiver;
|
|
2767
|
+
}
|
|
2768
|
+
else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('video') > -1) && replaceVideo && !videoTransceiver) {
|
|
2769
|
+
videoTransceiver = transceiver;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
if (replaceAudio) {
|
|
2774
|
+
config.stream.addTrack(stream.getAudioTracks()[0]);
|
|
2775
|
+
config.tracks.push(stream.getAudioTracks()[0]);
|
|
2776
|
+
if (audioTransceiver && audioTransceiver.sender) {
|
|
2777
|
+
audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]);
|
|
2778
|
+
} else {
|
|
2779
|
+
config.pc.addTrack(stream.getAudioTracks()[0], config.stream);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
else {
|
|
2783
|
+
if (audioTransceiver && audioTransceiver.sender) {
|
|
2784
|
+
audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(null);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
if (replaceVideo) {
|
|
2789
|
+
config.stream.addTrack(stream.getVideoTracks()[0]);
|
|
2790
|
+
config.tracks.push(stream.getVideoTracks()[0]);
|
|
2791
|
+
if (videoTransceiver && videoTransceiver.sender) {
|
|
2792
|
+
videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]);
|
|
2793
|
+
} else {
|
|
2794
|
+
if(!this.simulcast) {
|
|
2795
|
+
config.pc.addTrack(stream.getVideoTracks()[0], config.stream);
|
|
2796
|
+
}
|
|
2797
|
+
else {
|
|
2798
|
+
config.pc.addTransceiver(stream.getVideoTracks()[0], {
|
|
2799
|
+
direction: 'sendonly',
|
|
2800
|
+
streams: [config.stream],
|
|
2801
|
+
sendEncodings: structuredClone(simulcastConfigForSource?.bitrates)
|
|
2802
|
+
})
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
else {
|
|
2807
|
+
if (videoTransceiver && videoTransceiver.sender) {
|
|
2808
|
+
videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(null);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// we possibly created new transceivers, so we need to get them again
|
|
2813
|
+
|
|
2814
|
+
transceivers = config.pc.getTransceivers();
|
|
2815
|
+
existingTracks = [...(config.streamMap[this.id][source] || [])];
|
|
2816
|
+
if(!audioTransceiver) {
|
|
2817
|
+
audioTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && existingTracks.includes(transceiver.sender.track.id))
|
|
2818
|
+
}
|
|
2819
|
+
if(!videoTransceiver) {
|
|
2820
|
+
videoTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && existingTracks.includes(transceiver.sender.track.id))
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
let hasAudio = !!(stream && stream.getAudioTracks().length > 0);
|
|
2824
|
+
let hasVideo = !!(stream && stream.getVideoTracks().length > 0);
|
|
2825
|
+
|
|
2826
|
+
this.#setupTransceivers(this.#publisherHandle.handleId, [hasAudio, false, hasVideo, false, audioTransceiver, videoTransceiver]);
|
|
2827
|
+
|
|
2828
|
+
const emitEvents = () => {
|
|
2829
|
+
this.isPublished = true;
|
|
2830
|
+
|
|
2831
|
+
if(!config.stream.onremovetrack) {
|
|
2832
|
+
config.stream.onremovetrack = (ev) => {};
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
let republishTimeoutId = null;
|
|
2836
|
+
let tracks = config.stream.getTracks();
|
|
2837
|
+
|
|
2838
|
+
// this event is emitted when we publish a new source but more importantly if one of the sources goes away
|
|
2839
|
+
this.#emitLocalFeedUpdate({
|
|
2840
|
+
source
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
if(tracks.length) {
|
|
2844
|
+
tracks.forEach(track => {
|
|
2845
|
+
|
|
2846
|
+
// used as a flag to not emit tracks that been already emitted
|
|
2847
|
+
if(!track.onended) {
|
|
2848
|
+
|
|
2849
|
+
this.#emitLocalFeedUpdate({
|
|
2850
|
+
addingTrack: true,
|
|
2851
|
+
track,
|
|
2852
|
+
source
|
|
2853
|
+
})
|
|
2854
|
+
|
|
2855
|
+
track.onended = (ev) => {
|
|
2856
|
+
config.stream.removeTrack(track);
|
|
2857
|
+
config.tracks = config.tracks.filter(t => t.id !== track.id);
|
|
2858
|
+
this.#emitLocalFeedUpdate({
|
|
2859
|
+
removingTrack: true,
|
|
2860
|
+
track,
|
|
2861
|
+
source
|
|
2862
|
+
})
|
|
2863
|
+
clearTimeout(republishTimeoutId);
|
|
2864
|
+
republishTimeoutId = setTimeout(() => {
|
|
2865
|
+
this.#republishOnTrackEnded(source);
|
|
2866
|
+
}, 100);
|
|
2867
|
+
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
})
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
this.isMuted = [];
|
|
2874
|
+
for(const source of Object.keys(config.streamMap[this.id])) {
|
|
2875
|
+
const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
2876
|
+
const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
2877
|
+
this.isMuted.push({type: 'audio', value: !audioTrack || !audioTrack.enabled, source, mid: transceivers.find(transceiver => transceiver.sender?.track?.kind === 'audio' && transceiver.sender?.track?.id === audioTrack?.id)?.mid});
|
|
2878
|
+
this.isMuted.push({type: 'video', value: !videoTrack || !videoTrack.enabled, source, mid: transceivers.find(transceiver => transceiver.sender?.track?.kind === 'video' && transceiver.sender?.track?.id === videoTrack?.id)?.mid});
|
|
2879
|
+
|
|
2880
|
+
this.emit('localHasVideo', !!videoTrack, source);
|
|
2881
|
+
this.emit('localHasAudio', !!audioTrack, source);
|
|
2882
|
+
}
|
|
2883
|
+
for(const val of this.isMuted) {
|
|
2884
|
+
this.emit('localMuted', {...val});
|
|
2885
|
+
}
|
|
2886
|
+
this.emit('published', true);
|
|
2887
|
+
this.emit('publishing', false);
|
|
2888
|
+
|
|
2889
|
+
};
|
|
2890
|
+
|
|
2891
|
+
try {
|
|
2892
|
+
|
|
2893
|
+
await Promise.all([audioTrackReplacePromise, videoTrackReplacePromise]);
|
|
2894
|
+
|
|
2895
|
+
if(this._isDataChannelOpen !== true) {
|
|
2896
|
+
await new Promise((resolve, reject) => {
|
|
2897
|
+
let dataChannelTimeoutId = null;
|
|
2898
|
+
let _cleanup = () => {
|
|
2899
|
+
clearTimeout(dataChannelTimeoutId);
|
|
2900
|
+
this._abortController.signal.removeEventListener('abort', _rejectAbort);
|
|
2901
|
+
this.off('dataChannel', _resolve, this);
|
|
2902
|
+
}
|
|
2903
|
+
let _resolve = (val) => {
|
|
2904
|
+
if (val) {
|
|
2905
|
+
_cleanup();
|
|
2906
|
+
resolve(this);
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
let _rejectTimeout = () => {
|
|
2910
|
+
_cleanup();
|
|
2911
|
+
reject({type: 'error', id: 39, message: 'Data channel did not open', data: null});
|
|
2912
|
+
}
|
|
2913
|
+
let _rejectAbort = () => {
|
|
2914
|
+
_cleanup();
|
|
2915
|
+
reject({type: 'warning', id: 40, message: 'Connection cancelled'})
|
|
2916
|
+
}
|
|
2917
|
+
dataChannelTimeoutId = setTimeout(_rejectTimeout, 10000);
|
|
2918
|
+
this._abortController.signal.addEventListener('abort', _rejectAbort);
|
|
2919
|
+
this.on('dataChannel', _resolve, this);
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
emitEvents();
|
|
2924
|
+
|
|
2925
|
+
}
|
|
2926
|
+
catch(e) {
|
|
2927
|
+
this.emit('publishing', false);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
async unpublishLocal(dontWait = false) {
|
|
2934
|
+
return this.isPublished
|
|
2935
|
+
? this.sendMessage(this.#publisherHandle.handleId, {body: {"request": "unpublish"}}, dontWait)
|
|
2936
|
+
.finally(r => {
|
|
2937
|
+
this.isPublished = false;
|
|
2938
|
+
this.emit('published', false);
|
|
2939
|
+
return r
|
|
2940
|
+
})
|
|
2941
|
+
: Promise.resolve()
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
toggleAudio(value = null, source = 'camera0', mid) {
|
|
2945
|
+
let handle = this.#publisherHandle;
|
|
2946
|
+
if (!handle) {
|
|
2947
|
+
this.emit('error', {
|
|
2948
|
+
type: 'warning',
|
|
2949
|
+
id: 41,
|
|
2950
|
+
message: 'no local id, connect first', data: null
|
|
2951
|
+
});
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
let config = handle.webrtcStuff;
|
|
2955
|
+
let transceivers = config.pc?.getTransceivers();
|
|
2956
|
+
let transceiver = null;
|
|
2957
|
+
if(source) {
|
|
2958
|
+
transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (config.streamMap[this.id][source] || []).includes(t.sender.track.id));
|
|
2959
|
+
}
|
|
2960
|
+
else {
|
|
2961
|
+
transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (mid ? t.mid === mid : true));
|
|
2962
|
+
}
|
|
2963
|
+
if (transceiver) {
|
|
2964
|
+
transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
this.isMuted = [];
|
|
2968
|
+
for(const source of Object.keys(config.streamMap[this.id])) {
|
|
2969
|
+
const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
2970
|
+
const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
|
|
2971
|
+
const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
2972
|
+
const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
|
|
2973
|
+
this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
|
|
2974
|
+
this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
|
|
2975
|
+
}
|
|
2976
|
+
for(let val of this.isMuted) {
|
|
2977
|
+
this.emit('localMuted', {...val});
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
toggleVideo(value = null, source = 'camera0', mid) {
|
|
2982
|
+
let handle = this.#publisherHandle;
|
|
2983
|
+
if (!handle) {
|
|
2984
|
+
this.emit('error', {
|
|
2985
|
+
type: 'warning',
|
|
2986
|
+
id: 42,
|
|
2987
|
+
message: 'no local id, connect first', data: null
|
|
2988
|
+
});
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
let config = handle.webrtcStuff;
|
|
2992
|
+
let transceivers = config.pc?.getTransceivers();
|
|
2993
|
+
let transceiver = null;
|
|
2994
|
+
if(source) {
|
|
2995
|
+
transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (config.streamMap[this.id][source] || []).includes(t.sender.track.id));
|
|
2996
|
+
}
|
|
2997
|
+
else {
|
|
2998
|
+
transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (mid ? t.mid === mid : true));
|
|
2999
|
+
}
|
|
3000
|
+
if (transceiver) {
|
|
3001
|
+
transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
|
|
3002
|
+
}
|
|
3003
|
+
this.isMuted = [];
|
|
3004
|
+
for(const source of Object.keys(config.streamMap[this.id])) {
|
|
3005
|
+
const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
3006
|
+
const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
|
|
3007
|
+
const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
|
|
3008
|
+
const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
|
|
3009
|
+
this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
|
|
3010
|
+
this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
|
|
3011
|
+
}
|
|
3012
|
+
for(let val of this.isMuted) {
|
|
3013
|
+
this.emit('localMuted', {...val});
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
requestKeyFrame(handleId, mid) {
|
|
3018
|
+
this.sendMessage(handleId, {
|
|
3019
|
+
"body": {
|
|
3020
|
+
"request": "configure",
|
|
3021
|
+
"keyframe": true,
|
|
3022
|
+
...(mid !== undefined ? {streams: [{mid,keyframe:true}]} : {})
|
|
3023
|
+
}
|
|
3024
|
+
}).catch(() => null)
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
setRoomType(type = 'watchparty') {
|
|
3028
|
+
this._roomType = type;
|
|
3029
|
+
return this._roomType;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
#setSelectedSubstream(sender, mid, substream) {
|
|
3033
|
+
|
|
3034
|
+
const handle = this.#getHandle(sender);
|
|
3035
|
+
|
|
3036
|
+
if(!handle) {
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
handle.webrtcStuff.lastSwitchTime.set(mid, Date.now());
|
|
3041
|
+
|
|
3042
|
+
if(!handle.webrtcStuff.currentLayers.has(mid)) {
|
|
3043
|
+
handle.webrtcStuff.currentLayers.set(mid, {mid, substream: substream, temporal: -1});
|
|
3044
|
+
}
|
|
3045
|
+
else {
|
|
3046
|
+
handle.webrtcStuff.currentLayers.get(mid).substream = substream;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
#setSelectedTemporal(sender, mid, temporal) {
|
|
3051
|
+
|
|
3052
|
+
const handle = this.#getHandle(sender);
|
|
3053
|
+
|
|
3054
|
+
if(!handle) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
handle.webrtcStuff.lastSwitchTime.set(mid, Date.now());
|
|
3059
|
+
|
|
3060
|
+
if(!handle.webrtcStuff.currentLayers.has(mid)) {
|
|
3061
|
+
handle.webrtcStuff.currentLayers.set(mid, {mid, substream: -1, temporal: temporal});
|
|
3062
|
+
}
|
|
3063
|
+
else {
|
|
3064
|
+
handle.webrtcStuff.currentLayers.get(mid).temporal = temporal;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
#shouldEmitFeedUpdate(parsedDisplay = {}) {
|
|
3069
|
+
const myUserRole = decodeJanusDisplay(this.display)?.role || 'participant';
|
|
3070
|
+
const remoteUserRole = parsedDisplay?.role || 'participant';
|
|
3071
|
+
const remoteUserId = parsedDisplay?.userId;
|
|
3072
|
+
const shouldSubscribeToUserRole = this.userRoleSubscriptionRules[myUserRole][(this._roomType || 'watchparty')].indexOf(remoteUserRole) > -1
|
|
3073
|
+
const shouldSubscribeToUserId = this._restrictSubscribeToUserIds.length === 0 || this._restrictSubscribeToUserIds.indexOf(remoteUserId) > -1;
|
|
3074
|
+
return shouldSubscribeToUserRole && shouldSubscribeToUserId;
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
#shouldSubscribeParticipant(id, userId, description) {
|
|
3078
|
+
|
|
3079
|
+
const myUserRole = decodeJanusDisplay(this.display)?.role || 'participant';
|
|
3080
|
+
const remoteUserRole = decodeJanusDisplay(userId)?.role || 'participant';
|
|
3081
|
+
const remoteUserId = decodeJanusDisplay(userId)?.userId;
|
|
3082
|
+
const remoteIntercomGroups = description?.intercomGroups || [];
|
|
3083
|
+
const shouldSubscribeToUserRole = this.userRoleSubscriptionRules[myUserRole][(this._roomType || 'watchparty')].indexOf(remoteUserRole) > -1
|
|
3084
|
+
const shouldSubscribeToUserId = this._restrictSubscribeToUserIds.length === 0 || this._restrictSubscribeToUserIds.indexOf(remoteUserId) > -1;
|
|
3085
|
+
const shouldSubscribeToIntercomGroups = remoteIntercomGroups.some(g => this._listenIntercomChannels.indexOf(g) > -1);
|
|
3086
|
+
|
|
3087
|
+
return shouldSubscribeToUserRole && shouldSubscribeToUserId && shouldSubscribeToIntercomGroups;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
getUserTalkIntercomChannels(userId) {
|
|
3091
|
+
let talkIntercomChannels = []
|
|
3092
|
+
let transceiver = this.#subscriberHandle.webrtcStuff.transceiverMap.find(t => t.display && decodeJanusDisplay(t.display)?.userId === userId && t.description);
|
|
3093
|
+
|
|
3094
|
+
if(!transceiver) {
|
|
3095
|
+
return talkIntercomChannels;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
try {
|
|
3099
|
+
let description = JSON.parse(transceiver.description);
|
|
3100
|
+
if(description.intercomGroups) {
|
|
3101
|
+
description.intercomGroups.forEach(group => {
|
|
3102
|
+
if(!talkIntercomChannels.includes(group)) {
|
|
3103
|
+
talkIntercomChannels.push(group);
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
} catch (e) {}
|
|
3108
|
+
|
|
3109
|
+
return talkIntercomChannels;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
async setTalkIntercomChannels(groups = ['participants']) {
|
|
3113
|
+
|
|
3114
|
+
if(typeof groups !== 'object' || !("length" in groups)) {
|
|
3115
|
+
this._log('setTalkIntercomChannels: groups must be an array');
|
|
3116
|
+
groups = [groups];
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
this._talkIntercomChannels = structuredClone(groups);
|
|
3120
|
+
let handle = this.#getHandle(this.handleId);
|
|
3121
|
+
if (!handle) {
|
|
3122
|
+
return Promise.resolve();
|
|
3123
|
+
}
|
|
3124
|
+
let config = handle.webrtcStuff;
|
|
3125
|
+
let descriptions = [];
|
|
3126
|
+
let transceivers = config.pc.getTransceivers();
|
|
3127
|
+
Object.keys(config.streamMap[this.id]).forEach(source => {
|
|
3128
|
+
const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
|
|
3129
|
+
config.streamMap[this.id][source].forEach(trackId => {
|
|
3130
|
+
let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
|
|
3131
|
+
if(t) {
|
|
3132
|
+
descriptions.push({mid: t.mid, description: JSON.stringify({source, simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: this._talkIntercomChannels})});
|
|
3133
|
+
}
|
|
3134
|
+
})
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
return await this.sendMessage(this.handleId, {
|
|
3138
|
+
body: {
|
|
3139
|
+
"request": "configure",
|
|
3140
|
+
descriptions: descriptions,
|
|
3141
|
+
}
|
|
3142
|
+
})
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
async setListenIntercomChannels(groups = ['participants']) {
|
|
3146
|
+
|
|
3147
|
+
if(typeof groups !== 'object' || !("length" in groups)) {
|
|
3148
|
+
this._log('setListenIntercomChannels: groups must be an array');
|
|
3149
|
+
groups = [groups];
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
this._listenIntercomChannels = structuredClone(groups);
|
|
3153
|
+
|
|
3154
|
+
if(!this.isConnected) {
|
|
3155
|
+
return
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
return await this.#updateSubscriptions();
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
async setRestrictSubscribeToUserIds(userIds = []) {
|
|
3162
|
+
|
|
3163
|
+
if(typeof userIds !== 'object' || !("length" in userIds)) {
|
|
3164
|
+
this._log('setRestrictSubscribeToUserIds: userIds must be an array');
|
|
3165
|
+
userIds = [userIds];
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
this._restrictSubscribeToUserIds = structuredClone(userIds);
|
|
3169
|
+
|
|
3170
|
+
if(!this.isConnected) {
|
|
3171
|
+
return
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
return await this.#updateSubscriptions();
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
async selectSubStream(id, substream = 2, temporal = 2, source, mid) {
|
|
3178
|
+
|
|
3179
|
+
this._log('Select substream called for id:', id, 'Source, mid:', source, mid, 'Substream:', substream, 'Temporal:', temporal);
|
|
3180
|
+
|
|
3181
|
+
let config = this.#subscriberHandle.webrtcStuff;
|
|
3182
|
+
return new Promise((resolve, reject) => {
|
|
3183
|
+
let messageTimeoutId;
|
|
3184
|
+
|
|
3185
|
+
if(!config.streamMap[id]) {
|
|
3186
|
+
reject('no user found');
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
let clearListeners = () => {
|
|
3190
|
+
clearTimeout(messageTimeoutId);
|
|
3191
|
+
this._abortController.signal.removeEventListener('abort', abortResponse);
|
|
3192
|
+
this.off('longPollEvent', parseResponse);
|
|
3193
|
+
this.ws?.removeEventListener('message', parseResponse);
|
|
3194
|
+
};
|
|
3195
|
+
|
|
3196
|
+
let abortResponse = () => {
|
|
3197
|
+
clearListeners();
|
|
3198
|
+
reject('aborted');
|
|
3199
|
+
};
|
|
3200
|
+
|
|
3201
|
+
let parseResponse = (event) => {
|
|
3202
|
+
let json = typeof event.data === 'string'
|
|
3203
|
+
? JSON.parse(event.data)
|
|
3204
|
+
: event.data;
|
|
3205
|
+
|
|
3206
|
+
var sender = json["sender"];
|
|
3207
|
+
if(sender === this.#subscriberHandle.handleId) {
|
|
3208
|
+
let plugindata = json["plugindata"] || {};
|
|
3209
|
+
let msg = plugindata["data"] || {};
|
|
3210
|
+
let substream = msg["substream"];
|
|
3211
|
+
if(substream !== undefined && substream !== null && (mid !== undefined ? msg["mid"] === mid : true)) {
|
|
3212
|
+
clearListeners();
|
|
3213
|
+
resolve({substream, sender});
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
if(source !== undefined || mid !== undefined) {
|
|
3219
|
+
if(mid === undefined) {
|
|
3220
|
+
let transceivers = config.pc.getTransceivers();
|
|
3221
|
+
for(let trackId of config.streamMap[id][source]) {
|
|
3222
|
+
let transceiver = transceivers.find(transceiver => transceiver.receiver.track && transceiver.receiver.track.kind === 'video' && transceiver.receiver.track.id === trackId)
|
|
3223
|
+
if(transceiver) {
|
|
3224
|
+
mid = transceiver.mid;
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
if(mid !== undefined) {
|
|
3230
|
+
|
|
3231
|
+
this.on('longPollEvent', parseResponse);
|
|
3232
|
+
this.ws?.addEventListener('message', parseResponse);
|
|
3233
|
+
this._abortController.signal.addEventListener('abort', abortResponse);
|
|
3234
|
+
messageTimeoutId = setTimeout(() => {
|
|
3235
|
+
clearListeners();
|
|
3236
|
+
reject('timeout');
|
|
3237
|
+
}, 10000);
|
|
3238
|
+
|
|
3239
|
+
this.sendMessage(this.#subscriberHandle.handleId, {
|
|
3240
|
+
"body": {
|
|
3241
|
+
"request": "configure",
|
|
3242
|
+
"streams": [
|
|
3243
|
+
{
|
|
3244
|
+
mid, substream: parseInt(substream), temporal: parseInt(temporal)
|
|
3245
|
+
}
|
|
3246
|
+
]
|
|
3247
|
+
}
|
|
3248
|
+
})
|
|
3249
|
+
} else {
|
|
3250
|
+
reject('no mid found');
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
else {
|
|
3254
|
+
reject('no source or mid');
|
|
3255
|
+
}
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
async #getStats(type = 'video') {
|
|
3260
|
+
|
|
3261
|
+
let handle = this.#subscriberHandle;
|
|
3262
|
+
if(handle) {
|
|
3263
|
+
|
|
3264
|
+
const tracks = handle?.webrtcStuff?.tracks.filter(track => track.kind === type);
|
|
3265
|
+
const transceivers = handle?.webrtcStuff?.pc?.getTransceivers();
|
|
3266
|
+
return await tracks.reduce(async (prevPromise, track) => {
|
|
3267
|
+
|
|
3268
|
+
const results = await prevPromise;
|
|
3269
|
+
const mid = transceivers.find(t =>
|
|
3270
|
+
t.receiver?.track?.id === track.id || t.sender?.track?.id === track.id
|
|
3271
|
+
)?.mid;
|
|
3272
|
+
const transceiverData = this.#getTransceiverDataByMid(mid);
|
|
3273
|
+
|
|
3274
|
+
const id = transceiverData?.feed_id ?? null;
|
|
3275
|
+
const display = transceiverData?.feed_display ?? null;
|
|
3276
|
+
const description = transceiverData.feed_description ?? null;
|
|
3277
|
+
|
|
3278
|
+
return handle.webrtcStuff.pc.getStats(track)
|
|
3279
|
+
.then(stats => {
|
|
3280
|
+
|
|
3281
|
+
const parsedStats = {
|
|
3282
|
+
inboundRtpStats: null,
|
|
3283
|
+
remoteOutboundRtpStats: null,
|
|
3284
|
+
candidatePairStats: null,
|
|
3285
|
+
packetLoss: 0,
|
|
3286
|
+
jitter: 0,
|
|
3287
|
+
rtt: 0,
|
|
3288
|
+
currentBandwidth: 0,
|
|
3289
|
+
framesDecoded: 0,
|
|
3290
|
+
framesDropped: 0,
|
|
3291
|
+
framesReceived: 0,
|
|
3292
|
+
frameRate: 0,
|
|
3293
|
+
freezeCount: 0,
|
|
3294
|
+
totalFreezesDuration: 0,
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
let selectedCandidatePairId = null;
|
|
3298
|
+
|
|
3299
|
+
// First pass to identify the selected candidate pair ID from transport
|
|
3300
|
+
stats.forEach(stat => {
|
|
3301
|
+
if (stat.type === 'transport' && stat.selectedCandidatePairId) {
|
|
3302
|
+
selectedCandidatePairId = stat.selectedCandidatePairId;
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
3305
|
+
|
|
3306
|
+
// Second pass to process all stats
|
|
3307
|
+
stats.forEach(stat => {
|
|
3308
|
+
// Inbound RTP stats processing
|
|
3309
|
+
if (stat.type === 'inbound-rtp' && !stat.isRemote) {
|
|
3310
|
+
parsedStats.inboundRtpStats = stat;
|
|
3311
|
+
|
|
3312
|
+
// Consistent unit conversion - convert seconds to milliseconds for jitter
|
|
3313
|
+
parsedStats.jitter = stat.jitter ? Math.round(stat.jitter * 1000) : 0;
|
|
3314
|
+
|
|
3315
|
+
parsedStats.framesDecoded = stat.framesDecoded || 0;
|
|
3316
|
+
parsedStats.framesDropped = stat.framesDropped || 0;
|
|
3317
|
+
parsedStats.framesReceived = stat.framesReceived || 0;
|
|
3318
|
+
parsedStats.frameRate = stat.framesPerSecond || 0;
|
|
3319
|
+
parsedStats.freezeCount = stat.freezeCount || 0;
|
|
3320
|
+
parsedStats.totalFreezesDuration = stat.totalFreezesDuration || 0;
|
|
3321
|
+
|
|
3322
|
+
// Robust packet loss calculation
|
|
3323
|
+
if (stat.packetsLost !== undefined && stat.packetsReceived !== undefined) {
|
|
3324
|
+
const totalPackets = stat.packetsLost + stat.packetsReceived;
|
|
3325
|
+
if (totalPackets > 0) {
|
|
3326
|
+
// Calculate as percentage and round to 2 decimal places for accuracy
|
|
3327
|
+
parsedStats.packetLoss = Number(((stat.packetsLost / totalPackets) * 100).toFixed(2));
|
|
3328
|
+
} else {
|
|
3329
|
+
parsedStats.packetLoss = 0;
|
|
3330
|
+
}
|
|
3331
|
+
} else {
|
|
3332
|
+
parsedStats.packetLoss = 0;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
if (stat.type === 'remote-outbound-rtp') {
|
|
3337
|
+
parsedStats.remoteOutboundRtpStats = stat;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// Find active candidate pair based on the selectedCandidatePairId
|
|
3341
|
+
if (stat.type === 'candidate-pair' &&
|
|
3342
|
+
(stat.selected || stat.id === selectedCandidatePairId)) {
|
|
3343
|
+
parsedStats.candidatePairStats = stat;
|
|
3344
|
+
|
|
3345
|
+
// RTT calculation from candidate pair
|
|
3346
|
+
if (stat.currentRoundTripTime) {
|
|
3347
|
+
parsedStats.rtt = Math.round(stat.currentRoundTripTime * 1000); // Convert to ms
|
|
3348
|
+
} else if (stat.totalRoundTripTime && stat.responsesReceived > 0) {
|
|
3349
|
+
parsedStats.rtt = Math.round((stat.totalRoundTripTime / stat.responsesReceived) * 1000);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// Additional fallback for bandwidth estimation from inboundRtp
|
|
3355
|
+
if (parsedStats.currentBandwidth === 0 && parsedStats.inboundRtpStats) {
|
|
3356
|
+
const stat = parsedStats.inboundRtpStats;
|
|
3357
|
+
// Simple estimation based on received bytes over time
|
|
3358
|
+
if (stat.bytesReceived && stat.timestamp && handle.webrtcStuff.stats &&
|
|
3359
|
+
handle.webrtcStuff.stats[stat.id]) {
|
|
3360
|
+
const prevStat = handle.webrtcStuff.stats[stat.id];
|
|
3361
|
+
const timeDiff = stat.timestamp - prevStat.timestamp;
|
|
3362
|
+
if (timeDiff > 0) {
|
|
3363
|
+
const bitrateBps = 8000 * (stat.bytesReceived - prevStat.bytesReceived) / timeDiff;
|
|
3364
|
+
parsedStats.currentBandwidth = Math.floor(bitrateBps / 1000);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
// Store current stats for next calculation
|
|
3368
|
+
if (!handle.webrtcStuff.stats) {
|
|
3369
|
+
handle.webrtcStuff.stats = {};
|
|
3370
|
+
}
|
|
3371
|
+
handle.webrtcStuff.stats[stat.id] = { ...stat };
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
return [...results, {
|
|
3375
|
+
stats: Object.fromEntries(stats), // Convert MapLike object to regular object
|
|
3376
|
+
parsedStats,
|
|
3377
|
+
id,
|
|
3378
|
+
display,
|
|
3379
|
+
description,
|
|
3380
|
+
mid
|
|
3381
|
+
}];
|
|
3382
|
+
});
|
|
3383
|
+
}, Promise.resolve([]))
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
return [];
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
#adjustVideoQualitySettings(stats) {
|
|
3390
|
+
|
|
3391
|
+
// Only proceed if we have stats to work with and simulcast is enabled
|
|
3392
|
+
if (!stats?.length || !this.simulcast) {
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
const handle = this.#subscriberHandle;
|
|
3397
|
+
const webrtcStuff = handle?.webrtcStuff;
|
|
3398
|
+
|
|
3399
|
+
if(!handle) {
|
|
3400
|
+
return
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
const now = Date.now();
|
|
3404
|
+
|
|
3405
|
+
// Process each video stream's stats
|
|
3406
|
+
for (const stat of stats) {
|
|
3407
|
+
// Skip if no parsedStats or missing essential data
|
|
3408
|
+
if (!stat.parsedStats || !stat.mid || !stat.id || !stat.description) {
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
const mid = stat.mid;
|
|
3413
|
+
const feedId = stat.id;
|
|
3414
|
+
|
|
3415
|
+
// Parse description to get simulcast settings
|
|
3416
|
+
let simulcastConfig = null;
|
|
3417
|
+
try {
|
|
3418
|
+
const description = typeof stat.description === 'string' ?
|
|
3419
|
+
JSON.parse(stat.description) : stat.description;
|
|
3420
|
+
|
|
3421
|
+
if (description?.simulcastBitrates) {
|
|
3422
|
+
simulcastConfig = {
|
|
3423
|
+
bitrates: description.simulcastBitrates,
|
|
3424
|
+
defaultSubstream: 0 // Default to highest quality initially
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
this._log('Error parsing simulcast config:', e);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
// Skip if no simulcast config or we don't have layer tracking for this mid
|
|
3433
|
+
if (!simulcastConfig || !webrtcStuff?.currentLayers?.has(mid)) {
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
const currentLayer = webrtcStuff?.currentLayers?.get(mid);
|
|
3438
|
+
const currentSubstream = currentLayer.substream;
|
|
3439
|
+
const currentTemporal = currentLayer.temporal === -1 ? 2 : currentLayer.temporal;
|
|
3440
|
+
|
|
3441
|
+
// Initialize quality history for this mid if needed
|
|
3442
|
+
if (!webrtcStuff.qualityHistory.has(mid)) {
|
|
3443
|
+
webrtcStuff.qualityHistory.set(mid, []);
|
|
3444
|
+
webrtcStuff.lastSwitchTime.set(mid, 0);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// Extract network stats
|
|
3448
|
+
const {
|
|
3449
|
+
packetLoss,
|
|
3450
|
+
jitter,
|
|
3451
|
+
rtt,
|
|
3452
|
+
currentBandwidth,
|
|
3453
|
+
framesDropped,
|
|
3454
|
+
framesReceived,
|
|
3455
|
+
freezeCount,
|
|
3456
|
+
totalFreezesDuration
|
|
3457
|
+
} = stat.parsedStats;
|
|
3458
|
+
|
|
3459
|
+
// Calculated stats (like frame drop rate)
|
|
3460
|
+
const frameDropRate = framesReceived ? (framesDropped / framesReceived * 100) : 0;
|
|
3461
|
+
|
|
3462
|
+
// Add current quality measurement to history
|
|
3463
|
+
const qualityMeasure = {
|
|
3464
|
+
timestamp: now,
|
|
3465
|
+
packetLoss,
|
|
3466
|
+
jitter,
|
|
3467
|
+
rtt,
|
|
3468
|
+
currentBandwidth,
|
|
3469
|
+
frameDropRate,
|
|
3470
|
+
freezeCount,
|
|
3471
|
+
totalFreezesDuration
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
// Determine whether we need to switch qualities
|
|
3475
|
+
let targetSubstream = currentSubstream;
|
|
3476
|
+
|
|
3477
|
+
const history = webrtcStuff.qualityHistory.get(mid);
|
|
3478
|
+
history.push(qualityMeasure);
|
|
3479
|
+
|
|
3480
|
+
// Keep only recent history
|
|
3481
|
+
const recentHistory = history.filter(item => now - item.timestamp < this.#rtcStatsConfig.historySize);
|
|
3482
|
+
webrtcStuff.qualityHistory.set(mid, recentHistory);
|
|
3483
|
+
|
|
3484
|
+
let totalFreezesDurationTillLast = null;
|
|
3485
|
+
|
|
3486
|
+
// if we can calculate the freeze duration for last measured segment
|
|
3487
|
+
|
|
3488
|
+
if(recentHistory.length - 2 > -1) {
|
|
3489
|
+
|
|
3490
|
+
totalFreezesDurationTillLast = totalFreezesDuration - (recentHistory?.[recentHistory.length - 2]?.totalFreezesDuration ?? 0);
|
|
3491
|
+
if(totalFreezesDurationTillLast * 1000 >= this.#rtcStatsConfig.freezeLengthThreshold * this._statsInterval) {
|
|
3492
|
+
this._log(`Freezes detected for ${mid} - totalFreezesDuration: ${totalFreezesDuration}, totalFreezesDurationTillLast: ${totalFreezesDurationTillLast}`);
|
|
3493
|
+
if (currentSubstream > 0) {
|
|
3494
|
+
targetSubstream = currentSubstream - 1;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// Apply the layer change if needed
|
|
3498
|
+
// Don't change layers if we haven't waited enough since last switch
|
|
3499
|
+
|
|
3500
|
+
if (targetSubstream !== currentSubstream && now - webrtcStuff.lastSwitchTime.get(mid) >= this.#rtcStatsConfig.switchCooldownForFreeze) {
|
|
3501
|
+
webrtcStuff.lastSwitchTime.set(mid, now);
|
|
3502
|
+
this.selectSubStream(feedId, targetSubstream, currentTemporal, null, mid)
|
|
3503
|
+
.then(() => {
|
|
3504
|
+
this._log(`Successfully switched substream for ${mid} to ${targetSubstream}`);
|
|
3505
|
+
})
|
|
3506
|
+
.catch(err => {
|
|
3507
|
+
this._log(`Failed to switch substream for ${mid}:`, err);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// Don't change layers if we haven't waited enough since last switch
|
|
3515
|
+
if (now - webrtcStuff.lastSwitchTime.get(mid) < this.#rtcStatsConfig.switchCooldown) {
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// Get median values from recent history to avoid making decisions on outliers
|
|
3520
|
+
const medianPacketLoss = median(recentHistory.map(item => item.packetLoss));
|
|
3521
|
+
const medianJitter = median(recentHistory.map(item => item.jitter));
|
|
3522
|
+
const medianRtt = median(recentHistory.map(item => item.rtt));
|
|
3523
|
+
const medianBandwidth = median(recentHistory.map(item => item.currentBandwidth));
|
|
3524
|
+
const medianFrameDropRate = median(recentHistory.map(item => item.frameDropRate));
|
|
3525
|
+
|
|
3526
|
+
// Check for network issues that would require downgrading
|
|
3527
|
+
const hasHighPacketLoss = medianPacketLoss > this.#rtcStatsConfig.packetLossThreshold;
|
|
3528
|
+
const hasHighJitter = medianJitter > this.#rtcStatsConfig.jitterBufferThreshold;
|
|
3529
|
+
const hasHighRtt = medianRtt > this.#rtcStatsConfig.roundTripTimeThreshold;
|
|
3530
|
+
// Check if we have high frame drop rate
|
|
3531
|
+
const hasHighFrameDropRate = medianFrameDropRate > this.#rtcStatsConfig.frameDropRateThreshold; // 5% frame drop threshold
|
|
3532
|
+
|
|
3533
|
+
// Determine required bandwidth based on simulcast config
|
|
3534
|
+
let currentLayerBitrate = 0;
|
|
3535
|
+
let nextHigherLayerBitrate = 0;
|
|
3536
|
+
|
|
3537
|
+
// Bitrates are ordered from high to low (h, m, l)
|
|
3538
|
+
if (simulcastConfig?.bitrates?.length > currentSubstream) {
|
|
3539
|
+
currentLayerBitrate = simulcastConfig.bitrates[2 - currentSubstream].maxBitrate / 1000; // Convert to kbps
|
|
3540
|
+
|
|
3541
|
+
if (currentSubstream < simulcastConfig?.bitrates?.length - 1) {
|
|
3542
|
+
nextHigherLayerBitrate = simulcastConfig.bitrates[(2 - currentSubstream) - 1].maxBitrate / 1000;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// !!! WE CAN'T USE THIS AS IT REPORTS medianBandwidth of 1500 for 2000 stream and the stream works just fine
|
|
3547
|
+
// Check if we have enough bandwidth for current layer
|
|
3548
|
+
const hasLowBandwidthForCurrentLayer = medianBandwidth < (currentLayerBitrate * this.#rtcStatsConfig.badBandwidthThresholdMultiplier);
|
|
3549
|
+
|
|
3550
|
+
// Log stats for debugging
|
|
3551
|
+
this._log(`Stream ${mid} stats:`, {
|
|
3552
|
+
packetLoss: medianPacketLoss,
|
|
3553
|
+
jitter: medianJitter,
|
|
3554
|
+
rtt: medianRtt,
|
|
3555
|
+
bandwidth: medianBandwidth,
|
|
3556
|
+
frameDropRate: medianFrameDropRate,
|
|
3557
|
+
currentLayerBitrate,
|
|
3558
|
+
currentSubstream,
|
|
3559
|
+
totalFreezesDurationTillLast
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
// Network issues detected - downgrade if possible
|
|
3563
|
+
if (hasHighPacketLoss || hasHighJitter || hasHighRtt || hasHighFrameDropRate) {
|
|
3564
|
+
// Can't downgrade if already at lowest quality
|
|
3565
|
+
if (currentSubstream > 0) {
|
|
3566
|
+
targetSubstream = currentSubstream - 1;
|
|
3567
|
+
this._log(`Downgrading stream quality for ${mid} due to network issues:`, {
|
|
3568
|
+
packetLoss: medianPacketLoss,
|
|
3569
|
+
jitter: medianJitter,
|
|
3570
|
+
rtt: medianRtt,
|
|
3571
|
+
bandwidth: medianBandwidth,
|
|
3572
|
+
frameDropRate: medianFrameDropRate,
|
|
3573
|
+
from: currentSubstream,
|
|
3574
|
+
to: targetSubstream
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
// Check if we can upgrade - network is good
|
|
3579
|
+
else {
|
|
3580
|
+
|
|
3581
|
+
const stableHistory = recentHistory.filter(item => now - item.timestamp < this.#rtcStatsConfig.stableNetworkTime)
|
|
3582
|
+
const hasStableNetwork = stableHistory.every(item => item.packetLoss < this.#rtcStatsConfig.packetLossThreshold &&
|
|
3583
|
+
item.jitter < this.#rtcStatsConfig.jitterBufferThreshold &&
|
|
3584
|
+
item.rtt < this.#rtcStatsConfig.roundTripTimeThreshold &&
|
|
3585
|
+
item.frameDropRate < this.#rtcStatsConfig.frameDropRateThreshold
|
|
3586
|
+
);
|
|
3587
|
+
|
|
3588
|
+
if (hasStableNetwork) {
|
|
3589
|
+
if(currentSubstream < 2) {
|
|
3590
|
+
targetSubstream = currentSubstream + 1;
|
|
3591
|
+
this._log(`Upgrading stream quality for ${mid} due to good network conditions:`, {
|
|
3592
|
+
packetLoss: medianPacketLoss,
|
|
3593
|
+
jitter: medianJitter,
|
|
3594
|
+
rtt: medianRtt,
|
|
3595
|
+
bandwidth: medianBandwidth,
|
|
3596
|
+
from: currentSubstream,
|
|
3597
|
+
to: targetSubstream
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// Apply the layer change if needed
|
|
3604
|
+
if (targetSubstream !== currentSubstream) {
|
|
3605
|
+
webrtcStuff.lastSwitchTime.set(mid, now);
|
|
3606
|
+
this.selectSubStream(feedId, targetSubstream, currentTemporal, null, mid)
|
|
3607
|
+
.then(() => {
|
|
3608
|
+
this._log(`Successfully switched substream for ${mid} to ${targetSubstream}`);
|
|
3609
|
+
})
|
|
3610
|
+
.catch(err => {
|
|
3611
|
+
this._log(`Failed to switch substream for ${mid}:`, err);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
#emitRtcStats(stats) {
|
|
3618
|
+
if (stats?.length > 0) {
|
|
3619
|
+
const s = stats.map(stat => {
|
|
3620
|
+
const {
|
|
3621
|
+
currentBandwidth,
|
|
3622
|
+
frameRate,
|
|
3623
|
+
framesDecoded,
|
|
3624
|
+
framesDropped,
|
|
3625
|
+
framesReceived,
|
|
3626
|
+
freezeCount,
|
|
3627
|
+
jitter,
|
|
3628
|
+
packetLoss,
|
|
3629
|
+
rtt,
|
|
3630
|
+
totalFreezesDuration
|
|
3631
|
+
} = stat.parsedStats;
|
|
3632
|
+
|
|
3633
|
+
let description = {};
|
|
3634
|
+
try {
|
|
3635
|
+
description = JSON.parse(stat.description);
|
|
3636
|
+
} catch {}
|
|
3637
|
+
return {
|
|
3638
|
+
id: stat.id,
|
|
3639
|
+
mid: stat.mid,
|
|
3640
|
+
userId: decodeJanusDisplay(stat.display)?.userId,
|
|
3641
|
+
source: description.source,
|
|
3642
|
+
stats: {
|
|
3643
|
+
currentBandwidth,
|
|
3644
|
+
frameRate,
|
|
3645
|
+
framesDecoded,
|
|
3646
|
+
framesDropped,
|
|
3647
|
+
framesReceived,
|
|
3648
|
+
freezeCount,
|
|
3649
|
+
jitter,
|
|
3650
|
+
packetLoss,
|
|
3651
|
+
rtt,
|
|
3652
|
+
totalFreezesDuration
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3656
|
+
this.emit('rtcStats', s);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
#disableStatsWatch() {
|
|
3661
|
+
if (this._statsIntervalId) {
|
|
3662
|
+
clearInterval(this._statsIntervalId);
|
|
3663
|
+
this._statsIntervalId = null;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
#enableStatsWatch() {
|
|
3668
|
+
|
|
3669
|
+
this.#disableStatsWatch();
|
|
3670
|
+
|
|
3671
|
+
const loop = () => {
|
|
3672
|
+
this.#getStats('video')
|
|
3673
|
+
.then(stats => {
|
|
3674
|
+
if (stats && stats.length > 0) {
|
|
3675
|
+
this.#emitRtcStats(stats)
|
|
3676
|
+
if(this.options.enableVideoQualityAdjustment) {
|
|
3677
|
+
this.#adjustVideoQualitySettings(stats);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
})
|
|
3681
|
+
};
|
|
3682
|
+
|
|
3683
|
+
this._statsIntervalId = setInterval(loop, this._statsInterval);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
|
|
3689
|
+
export default Room;
|
|
3690
|
+
|
|
3691
|
+
|
|
3692
|
+
|