@telegenta/webclient 2.0.11 → 3.0.0

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.
Files changed (102) hide show
  1. package/README.md +102 -81
  2. package/demos/add_phone_number.html +8 -7
  3. package/demos/calldata_available.html +5 -5
  4. package/demos/embedded_config.html +95 -95
  5. package/demos/event_client.html +226 -226
  6. package/demos/phone_client.html +564 -558
  7. package/dist/telegenta.js +3627 -0
  8. package/dist/telegenta.min.js +1 -0
  9. package/docs/EventClient.html +3 -1093
  10. package/docs/InvalidMediaDevice.html +3 -0
  11. package/docs/InvalidOptions.html +3 -0
  12. package/docs/InvalidParameter.html +3 -0
  13. package/docs/LogHandler.html +7 -0
  14. package/docs/MediaNotPrepared.html +3 -0
  15. package/docs/NoActiveSession.html +3 -0
  16. package/docs/NotAllowed.html +3 -0
  17. package/docs/NotReady.html +3 -0
  18. package/docs/Phone.html +4 -2837
  19. package/docs/Request.html +3 -0
  20. package/docs/Requestor.html +3 -0
  21. package/docs/Session.html +3 -2599
  22. package/docs/UnableToConnect.html +3 -0
  23. package/docs/data/search.json +1 -0
  24. package/docs/event-client.js.html +346 -0
  25. package/docs/exceptions.js.html +94 -0
  26. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  27. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  28. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  29. package/docs/global.html +5 -4950
  30. package/docs/index.html +3 -66
  31. package/docs/logger.js.html +106 -0
  32. package/docs/phone-client.js.html +2155 -0
  33. package/docs/requestor.js.html +223 -0
  34. package/docs/scripts/core.js +726 -0
  35. package/docs/scripts/core.min.js +23 -0
  36. package/docs/scripts/resize.js +90 -0
  37. package/docs/scripts/search.js +265 -0
  38. package/docs/scripts/search.min.js +6 -0
  39. package/docs/scripts/{prettify → third-party}/Apache-License-2.0.txt +202 -202
  40. package/docs/scripts/third-party/fuse.js +9 -0
  41. package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
  42. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  43. package/docs/scripts/third-party/hljs-original.js +5171 -0
  44. package/docs/scripts/third-party/hljs.js +1 -0
  45. package/docs/scripts/third-party/popper.js +5 -0
  46. package/docs/scripts/third-party/tippy.js +1 -0
  47. package/docs/scripts/third-party/tocbot.js +672 -0
  48. package/docs/scripts/third-party/tocbot.min.js +1 -0
  49. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  50. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  51. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  52. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  53. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  54. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  55. package/package.json +46 -34
  56. package/babel.config.js +0 -16
  57. package/browser/telegenta.min.js +0 -1
  58. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  59. package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
  60. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  61. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  62. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  63. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  64. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  65. package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
  66. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  67. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  68. package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
  69. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  70. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  71. package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  72. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  73. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  74. package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
  75. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  76. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  77. package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  78. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  79. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  80. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  81. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  82. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  83. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  84. package/docs/scripts/linenumber.js +0 -25
  85. package/docs/scripts/prettify/lang-css.js +0 -2
  86. package/docs/scripts/prettify/prettify.js +0 -28
  87. package/docs/styles/jsdoc-default.css +0 -692
  88. package/docs/styles/prettify-jsdoc.css +0 -111
  89. package/docs/styles/prettify-tomorrow.css +0 -132
  90. package/embedded_client/embedded_phone.js +0 -325
  91. package/embedded_client/index.html +0 -201
  92. package/embedded_client/server.php +0 -28
  93. package/embedded_client/styles.css +0 -310
  94. package/embedded_client/tiny_rest_client.php +0 -73
  95. package/embedded_client/webphone_sprites.png +0 -0
  96. package/es5/event-client.js +0 -322
  97. package/es5/index.js +0 -44
  98. package/es5/phone-client.js +0 -1739
  99. package/src/event-client.js +0 -1
  100. package/src/index.js +0 -10
  101. package/src/phone-client.js +0 -1
  102. package/webpack.config.js +0 -39
@@ -0,0 +1,3627 @@
1
+ import EventEmitter from 'events';
2
+ import JsSIP from 'jssip';
3
+
4
+ /*eslint no-console: ["error", { allow: ["debug", "info", "warn", "error", "log"] }] */
5
+
6
+ /**
7
+ * Logging class for Telegenta library. Use this if you want to handle logs or control log level.
8
+ * If no handler is given, logs will be sent to console.
9
+ *
10
+ * @example
11
+ * // Creating Phone object with custom log instance
12
+ * import {LogHandler, LOGLVL} from './telegenta.js';
13
+ * const myLogger = new LogHandler(LOGLVL.WARNING, null)
14
+ * const phone = new Telegenta.Phone(myLogger)
15
+ * ...
16
+ */
17
+ class LogHandler {
18
+ /** Create new log handler
19
+ * @param {LOGLVL} level - Log level
20
+ * @param {function} handler - External handler function to call on new log entries
21
+ */
22
+ constructor(level = 2, handler = null) {
23
+ this.extHandler = handler;
24
+ this.level = level;
25
+ }
26
+
27
+ debug(...args) {
28
+ this._doLog(LOGLVL$1.DEBUG, args);
29
+ }
30
+
31
+ info(...args) {
32
+ this._doLog(LOGLVL$1.INFO, args);
33
+ }
34
+
35
+ warn(...args) {
36
+ this._doLog(LOGLVL$1.WARN, args);
37
+ }
38
+
39
+ warning(...args) {
40
+ this._doLog(LOGLVL$1.WARN, args);
41
+ }
42
+
43
+ error(...args) {
44
+ this._doLog(LOGLVL$1.ERROR, args);
45
+ }
46
+
47
+ _doLog(level, args) {
48
+ // Check log level
49
+ if (level > this.level) {
50
+ return;
51
+ }
52
+
53
+ // If only one element, discard array (gives better output to console)
54
+ if (args.length === 1) {
55
+ args = args[0];
56
+ }
57
+
58
+ // Use external handler?
59
+ if (this.extHandler) {
60
+ this.extHandler(LOGLVLSTR[level], args);
61
+ } else {
62
+ switch (level) {
63
+ case LOGLVL$1.DEBUG:
64
+ console.debug(args);
65
+ break;
66
+
67
+ case LOGLVL$1.INFO:
68
+ console.info(args);
69
+ break;
70
+
71
+ case LOGLVL$1.WARN:
72
+ console.warn(args);
73
+ break;
74
+
75
+ case LOGLVL$1.ERROR:
76
+ console.error(args);
77
+ break;
78
+
79
+ default:
80
+ console.log(args);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Log levels
88
+ * @typedef LOGLVL {number}
89
+ * @property {number} ERROR 0 Error messages
90
+ * @property {number} WARN 1 Warning meesages
91
+ * @property {number} INFO 2 Informational messages (default)
92
+ * @property {number} DEBUG 3 Debug messages
93
+ */
94
+ const LOGLVL$1 = {
95
+ DEBUG: 3,
96
+ INFO: 2,
97
+ WARN: 1,
98
+ ERROR: 0,
99
+ };
100
+
101
+ const LOGLVLSTR = {
102
+ 3: "DEBUG",
103
+ 2: "INFO ",
104
+ 1: "WARN ",
105
+ 0: "ERROR",
106
+ };
107
+
108
+ /**
109
+ * Exception raised if Phone media permission is not prepared / allowed
110
+ */
111
+ class MediaNotPrepared extends Error {
112
+ constructor(message) {
113
+ super();
114
+ this.name = 'MediaNotPrepared';
115
+ this.message = message;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Exception raised if Phone is not ready to perform desired action
121
+ */
122
+ class NotReady extends Error {
123
+ constructor(message) {
124
+ super();
125
+ this.name = 'NotReady';
126
+ this.message = message;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Exception raised if media device id given does not exists
132
+ * @property message - Error message
133
+ */
134
+ class InvalidMediaDevice extends Error {
135
+ constructor(message) {
136
+ super();
137
+ this.name = 'InvalidMediaDevice';
138
+ this.message = message;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Exception raised if one or more options parsed is invalid
144
+ * @property param - Name of invalid options parameter
145
+ * @property message - Error message
146
+ *
147
+ */
148
+ class InvalidOptions extends Error {
149
+ constructor(param, message) {
150
+ super();
151
+ this.name = 'InvalidOptions';
152
+ this.param = param;
153
+ this.message = message;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Exception raised if parameter given is invalid
159
+ * @property message - Error message
160
+ */
161
+ class InvalidParameter extends Error {
162
+ constructor(message) {
163
+ super();
164
+ this.name = 'InvalidParameter';
165
+ this.message = message;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Exception raised if trying to perform actions that require active session
171
+ */
172
+ class NoActiveSession extends Error {
173
+ constructor() {
174
+ super();
175
+ this.name = 'NoActiveSession';
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Exception raised if unable to connect to service
181
+ */
182
+ class UnableToConnect extends Error {
183
+ constructor() {
184
+ super();
185
+ this.name = 'UnableToConnect';
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Exception raised operation is not allowed
191
+ */
192
+ class NotAllowed extends Error {
193
+ constructor(message) {
194
+ super();
195
+ this.name = 'NotAllowed';
196
+ this.message = message;
197
+ }
198
+ }
199
+
200
+ /*
201
+ * TOOLBOX
202
+ */
203
+
204
+ // Create a new UUID
205
+ function createUUID() {
206
+ let d = new Date().getTime();
207
+ if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
208
+ d += performance.now(); //use high-precision timer if available
209
+ }
210
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
211
+ let r = (d + Math.random() * 16) % 16 | 0;
212
+ d = Math.floor(d / 16);
213
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
214
+ });
215
+ }
216
+
217
+ // Compare properties of two objects. Comparison check if all properties in obj1 exists and has same values in obj2
218
+ function compareObjects(obj1, obj2) {
219
+ if ((!obj1) || (!obj2)) {
220
+ return false;
221
+ }
222
+
223
+ // console.info('OBJECT 1: '+JSON.stringify(obj1));
224
+ // console.info('OBJECT 2: '+JSON.stringify(obj2));
225
+
226
+ const k2 = Object.keys(obj2);
227
+ return Object.keys(obj1).every(function (k) {
228
+ // Check key exists first
229
+ if (k2.indexOf(k) === -1) {
230
+ return false;
231
+ }
232
+
233
+ return (obj1[k] === obj2[k]);
234
+ });
235
+ }
236
+
237
+ // Return duration as formatted string
238
+ function createDurationString(milliSeconds) {
239
+ const seconds = Math.floor(milliSeconds / 1000);
240
+
241
+ let s = '';
242
+
243
+ // Days
244
+ if (seconds / 86400 >= 1) {
245
+ s = s + Math.floor(seconds / 86400) + 'd ';
246
+ }
247
+ // Hour
248
+ if (seconds / 3600 >= 1) {
249
+ s = s + Math.floor(seconds / 3600) % 24 + 'h ';
250
+ }
251
+
252
+ // Seconds
253
+ if (seconds >= 0) {
254
+ s = s + ("00" + Math.floor(seconds / 60) % 60).slice(-2) + ':';
255
+ s = s + ("00" + (seconds % 60)).slice(-2);
256
+ } else {
257
+ s = '00:00';
258
+ }
259
+
260
+ return s;
261
+ }
262
+
263
+ // Browser checks
264
+ function browserIsOpera() {
265
+ return ((navigator.userAgent.indexOf("Opera") || navigator.userAgent.indexOf('OPR')) !== -1)
266
+ }
267
+
268
+ function browserIsChrome() {
269
+ return (navigator.userAgent.indexOf("Chrome") !== -1)
270
+ }
271
+
272
+ function browserIsFirefox() {
273
+ return (navigator.userAgent.indexOf("Firefox") !== -1)
274
+ }
275
+
276
+ function browserIsSafari() {
277
+ return (navigator.userAgent.indexOf("Safari") !== -1)
278
+ }
279
+
280
+ function browserIsEdge() {
281
+ return (navigator.userAgent.indexOf("Edge") !== -1)
282
+ }
283
+
284
+ // Request timeout after 10 sec
285
+ const REQUEST_TIMEOUT = 10000;
286
+
287
+ /**
288
+ * Request class passed to event listeners
289
+ * Can be used to fulfill request and receive request parameters (data property)
290
+ */
291
+ class Request {
292
+ constructor(owner, socket, request) {
293
+ this.socket = socket;
294
+ this._owner = owner;
295
+ this._request = request;
296
+ this._handled = false;
297
+ this.data = request.data;
298
+ }
299
+
300
+ /**
301
+ * Resolve request (promise will be resolved successfully on remote)
302
+ * @param data
303
+ */
304
+ resolve(data = null) {
305
+ this._assertSingleRequest();
306
+ try {
307
+ this.socket.send(JSON.stringify({ type: "_ACK", id: this._request.id, data: data }));
308
+ } catch (e) {
309
+ this._owner.log.warning(`Unable to resolve request ${this._request.id}: ${e}`);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Reject request (promise will be rejected on remote)
315
+ * @param reason
316
+ */
317
+ reject(reason = null) {
318
+ this._assertSingleRequest();
319
+ try {
320
+ this.socket.send(JSON.stringify({ type: "_NACK", id: this._request.id, reason: reason }));
321
+ } catch (e) {
322
+ this._owner.log.warning(`Unable to reject request ${this._request.id}: ${e}`);
323
+ }
324
+ }
325
+
326
+ // Make sure request is only fulfilled once
327
+ _assertSingleRequest() {
328
+ if (this._handled) {
329
+ throw new NotAllowed(`Request ${this._request.id} already fulfilled`);
330
+ }
331
+ this._handled = true;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * This class handles async communication on websockets and implements promises for requests
337
+ */
338
+ class Requestor extends EventEmitter {
339
+ constructor(logger = null) {
340
+ super();
341
+ this.log = logger ? logger : new LogHandler(LOGLVL$1.INFO);
342
+ this._pendingRequests = [];
343
+ }
344
+
345
+ /**
346
+ * Receive message from websocket
347
+ * @param socket
348
+ * @param message
349
+ */
350
+ receiveMessage(socket, message) {
351
+ this.log.debug("Received message: " + message);
352
+
353
+ let msg;
354
+ try {
355
+ msg = JSON.parse(message);
356
+ } catch (err) {
357
+ this.log.warning("Unable to parse message: " + message);
358
+ return;
359
+ }
360
+
361
+ // Validate msg fields
362
+ if (!msg.hasOwnProperty("type") || !msg.hasOwnProperty("id")) {
363
+ this.log.warning(`Invalid message received: ${msg} - missing type or id`);
364
+ return;
365
+ }
366
+
367
+ // Is it a ACK or NACK response?
368
+ if (msg.type === "_ACK" || msg.type === "_NACK") {
369
+ // Find pending request first
370
+ const request = this.findPendingRequest(msg.id);
371
+ if (!request) {
372
+ this.log.warning(`Received message with invalid request id: ${msg.id} - timed out?`);
373
+ return;
374
+ }
375
+
376
+ if (msg.type === "_ACK") {
377
+ this.log.debug(`Received ACK with id=${msg.id}`);
378
+ request.resolve(msg.data);
379
+ } else if (msg.type === "_NACK") {
380
+ this.log.debug(`Received NACK with id=${msg.id}`);
381
+ request.reject(msg.reason);
382
+ }
383
+
384
+ this.removePendingRequest(msg.id);
385
+ return;
386
+ }
387
+
388
+ // Is it PING request?
389
+ if (msg.type === "_PING") {
390
+ this.log.debug("Received ping, sending pong (ACK)");
391
+ socket.send(
392
+ JSON.stringify({
393
+ type: "_ACK",
394
+ id: msg.id,
395
+ data: "pong",
396
+ }),
397
+ );
398
+ return;
399
+ }
400
+
401
+ this.log.debug(`Received new request of type '${msg.type}' with id=${msg.id}`);
402
+ if (!this.emit(msg.type, new Request(this, socket, msg))) {
403
+ this.log.warning("Received request with no listeners, rejecting");
404
+ socket.send(
405
+ JSON.stringify({
406
+ type: "_NACK",
407
+ id: msg.id,
408
+ reason: "No handlers for that request type",
409
+ }),
410
+ );
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Send a request
416
+ * @param socket Socket to send request
417
+ * @param type Request type name
418
+ * @param data Request Payload
419
+ * @throws InvalidParameter - If one or more parameters specified is invalid
420
+ * @returns promise Fulfilled once request has been handled on remote or rejected on error
421
+ */
422
+ sendRequest(socket, type, data = {}) {
423
+ if (!socket || !type) {
424
+ throw new InvalidParameter("You must specify socket and request type");
425
+ }
426
+
427
+ // Build and send request message
428
+ let r = { type: type, id: createUUID(), data: data };
429
+ this.log.debug(`Sending request type '${type}' with id ${r.id}`);
430
+ try {
431
+ socket.send(JSON.stringify(r));
432
+ } catch (e) {
433
+ this.log.warning(`Unable to send request ${r.id} socket error: ${e}`);
434
+ return new Promise(function (resolve, reject) {
435
+ reject(`Unable to send request: ${e}`);
436
+ });
437
+ }
438
+
439
+ // Return promise and push to pending request list with a timeout
440
+ const self = this;
441
+ return new Promise(function (resolve, reject) {
442
+ self._pendingRequests.push({
443
+ request: r,
444
+ resolve: resolve,
445
+ reject: reject,
446
+ timeoutObj: setTimeout(function () {
447
+ self.log.warning(`Timeout on request ${r.id}, rejecting!`);
448
+ self.removePendingRequest(r.id);
449
+ reject("Request timed out");
450
+ }, REQUEST_TIMEOUT),
451
+ });
452
+ });
453
+ }
454
+
455
+ /**
456
+ * Send a ping request (for keep alives)
457
+ * @param socket
458
+ * @returns promise Promise resolved on reply
459
+ */
460
+ ping(socket) {
461
+ this.log.debug("Sending ping");
462
+ return this.sendRequest(socket, "_PING");
463
+ }
464
+
465
+ /**
466
+ * Remove a request from pending queue
467
+ * @param requestId
468
+ * @returns boolean Removal outcome (true = removed)
469
+ */
470
+ removePendingRequest(requestId) {
471
+ for (let idx in this._pendingRequests) {
472
+ if (this._pendingRequests[idx].request.id === requestId) {
473
+ clearTimeout(this._pendingRequests[idx].timeoutObj);
474
+ this._pendingRequests.splice(idx, 1);
475
+ this.log.debug(`Removed request id: ${requestId}`);
476
+ return true;
477
+ }
478
+ }
479
+
480
+ this.log.warning(`Unable to remove request ${requestId} => not found`);
481
+ return false;
482
+ }
483
+
484
+ /**
485
+ * Get index og pending request
486
+ * @param requestId
487
+ * @returns object Return request from pending request array or null if not found
488
+ */
489
+ findPendingRequest(requestId) {
490
+ for (let req of this._pendingRequests) {
491
+ if (req.request.id === requestId) {
492
+ return req;
493
+ }
494
+ }
495
+ return null;
496
+ }
497
+ }
498
+
499
+ /*
500
+ * EVENT CLASS
501
+ *
502
+ * AMQP Message structure
503
+ * ----------------------
504
+ * event_name
505
+ * customer_id
506
+ * partner_id
507
+ * payload
508
+ * resource_name (only resource_changed)
509
+ * call_token (only calldata_available)
510
+ *
511
+ */
512
+ // Keep alive in seconds
513
+ const PING_INTERVAL = 50;
514
+
515
+ const ES_DEFAULT_SERVER_URL = "wss://api.telegenta.com/es/";
516
+
517
+ // Starting retry interval
518
+ const RETRY_INTERVAL_START = 1 + Math.random();
519
+
520
+ // Maximum reconnect time
521
+ const RETRY_MAX_BACKOFF = 300;
522
+
523
+ class EventSubscription {
524
+ constructor(id, specification, callback) {
525
+ this.id = id;
526
+ this.specification = specification;
527
+ this.callback = callback;
528
+ }
529
+
530
+ doCallback(context, payload) {
531
+ return this.callback.apply(context, [payload]);
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Class is responsible for handling real time server events over websockets <br>
537
+ * Using this class you can subscribe to a list of useful remote events
538
+ *
539
+ * @param {LogHandler} [logger] - Custom log instance
540
+ *
541
+ */
542
+ class EventClient {
543
+ constructor(logger = null) {
544
+ this._socket = null;
545
+ this._previousSubscriptions = [];
546
+ this._activeSubscriptions = [];
547
+ this._pingTimerObj = null;
548
+ this._autoReconnect = false;
549
+ this._reconnectRetrySeconds = RETRY_INTERVAL_START;
550
+ this.log = logger ? logger : new LogHandler(LOGLVL.ERROR);
551
+ this._requestor = new Requestor(this.log);
552
+
553
+ this._requestor.on("event", (request) => {
554
+ this._handleIncomingEvent(request);
555
+ });
556
+ }
557
+
558
+ /**
559
+ * Establish a connecting to the event service
560
+ * @param {string=} Event service address URL
561
+ * @returns object Connection promise
562
+ */
563
+ connect(server_address = null) {
564
+ if (!server_address) {
565
+ server_address = ES_DEFAULT_SERVER_URL;
566
+ }
567
+
568
+ this._serverAddress = server_address;
569
+ this._previousSubscriptions = [];
570
+ this._activeSubscriptions = [];
571
+ return this._connectServer();
572
+ }
573
+
574
+ /**
575
+ * Disconnect from event service and clear all active subscriptions
576
+ * @throws NoActiveSession if not connected
577
+ */
578
+ disconnect() {
579
+ if (this._socket.readyState !== 1) {
580
+ throw new NoActiveSession();
581
+ }
582
+
583
+ this._activeSubscriptions = [];
584
+ this._autoReconnect = false;
585
+ this._socket.close();
586
+ }
587
+
588
+ /**
589
+ * Return service connection state
590
+ * @returns {boolean}
591
+ */
592
+ isConnected() {
593
+ if (this._socket) {
594
+ return this._socket.readyState === 1;
595
+ } else {
596
+ return false;
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Subscribe to event
602
+ * @param eventSpecification object Event parameters (please check docs for further description)
603
+ * @param callback object Callback function (will be called when event is received)
604
+ * @throws InvalidParameter
605
+ * @returns object Promise on subscription
606
+ */
607
+ subscribe(eventSpecification, callback) {
608
+ if (!eventSpecification || !callback) {
609
+ throw new InvalidParameter("You must specify both event spec and callback function");
610
+ }
611
+
612
+ if (!eventSpecification.hasOwnProperty("event_name")) {
613
+ throw new InvalidParameter("Your event specification must contain event name");
614
+ }
615
+
616
+ if (!this.isConnected()) {
617
+ throw new NoActiveSession();
618
+ }
619
+
620
+ const self = this;
621
+ return new Promise(function (resolve, reject) {
622
+ self._requestor.sendRequest(self._socket, "subscribe", eventSpecification).then(
623
+ function (subscriptionId) {
624
+ self.log.debug(`Subscription to '${eventSpecification.event_name}' was successful, ID = ${subscriptionId}`);
625
+ self._activeSubscriptions.push(new EventSubscription(subscriptionId, eventSpecification, callback));
626
+ resolve(subscriptionId);
627
+ },
628
+ function (reason) {
629
+ self.log.warning(`Subscription to '${eventSpecification.event_name}' failed: ${reason}`);
630
+ reject(reason);
631
+ },
632
+ );
633
+ });
634
+ }
635
+
636
+ /**
637
+ * Unsubscribe event messages
638
+ * @param subscriptionId Object Event specification to unsubscribe (must match specification used on subscribe)
639
+ * @throws InvalidParameter
640
+ * @returns object Promise on unsubscribe
641
+ */
642
+ unsubscribe(subscriptionId) {
643
+ if (!subscriptionId) {
644
+ throw new InvalidParameter("You must specify subscription id");
645
+ }
646
+
647
+ if (!this.isConnected()) {
648
+ throw new NoActiveSession();
649
+ }
650
+
651
+ this.log.info("Unsubscribe id: " + subscriptionId);
652
+
653
+ const self = this;
654
+ return new Promise(function (resolve, reject) {
655
+ self._requestor.sendRequest(self._socket, "unsubscribe", { id: subscriptionId }).then(
656
+ (data) => {
657
+ self.log.info(`Unsubscribe to '${subscriptionId}' was successful`);
658
+ self._removeSubscription(subscriptionId);
659
+ resolve(data);
660
+ },
661
+ (reason) => {
662
+ self.log.warning(`Remote unsubscribe to '${subscriptionId}' failed: ${reason}`);
663
+ reject(reason);
664
+ },
665
+ );
666
+ });
667
+ }
668
+
669
+ /**
670
+ * Fire event message
671
+ * @param type Event type to fire
672
+ * @param payload Event payload
673
+ * @throws NoActiveSession
674
+ * @returns object Promise on event
675
+ */
676
+ fireEvent(type, payload) {
677
+ if (!this.isConnected()) {
678
+ throw new NoActiveSession();
679
+ }
680
+
681
+ return new Promise((resolve, reject) => {
682
+ this._requestor.sendRequest(this._socket, type, payload).then(
683
+ (data) => {
684
+ this.log.info(`Event ${type} was successful`);
685
+ resolve(data);
686
+ },
687
+ (reason) => {
688
+ this.log.warning(`Event ${type} failed: ${reason}`);
689
+ reject(reason);
690
+ },
691
+ );
692
+ });
693
+ }
694
+
695
+ // Remove subscription from active subscriptions
696
+ _removeSubscription(subscriptionId) {
697
+ for (let idx in this._activeSubscriptions) {
698
+ if (this._activeSubscriptions[idx].id === subscriptionId) {
699
+ this.log.debug(`Removed local subscription: ${subscriptionId}`);
700
+ this._activeSubscriptions.splice(idx, 1);
701
+ return true;
702
+ }
703
+ }
704
+
705
+ this.log.warning(`Unable to remove subscription ${subscriptionId} locally, not found?`);
706
+ return false;
707
+ }
708
+
709
+ // Connect to event service
710
+ _connectServer() {
711
+ this.log.info("Connecting to event server: " + this._serverAddress);
712
+
713
+ const self = this;
714
+ return new Promise(function (resolve, reject) {
715
+ if (self._socket) {
716
+ switch (self._socket.readyState) {
717
+ case 0:
718
+ reject("Unable to connect, already connecting");
719
+ return;
720
+ case 1:
721
+ reject("Unable to connect, already connected");
722
+ return;
723
+ case 2:
724
+ reject("Unable to connect, socket is still closing");
725
+ return;
726
+ }
727
+ }
728
+
729
+ self._socket = new WebSocket(self._serverAddress);
730
+ let was_connected = false;
731
+
732
+ // CONNECTION OPEN
733
+ self._socket.addEventListener("open", function (/*event*/) {
734
+ self.log.info("Successfully connected to event service");
735
+
736
+ was_connected = true;
737
+ // After successful connect we enable auto connect and reset reconnect timer
738
+ self._autoReconnect = true;
739
+ self._reconnectRetrySeconds = RETRY_INTERVAL_START;
740
+
741
+ // Hook up event for incoming messages to requestor
742
+ self._socket.addEventListener("message", function (event) {
743
+ self._requestor.receiveMessage(self._socket, event.data);
744
+ });
745
+
746
+ // Start keep alive timer
747
+ clearTimeout(self._pingTimerObj);
748
+ self._pingTimerObj = setInterval(function () {
749
+ self._requestor.ping(self._socket).then(
750
+ function () {
751
+ // self.log.debug('Pong');
752
+ },
753
+ function (reason) {
754
+ self.log.warning("Ping failed: " + reason);
755
+ self._reconnectServer();
756
+ },
757
+ );
758
+ }, PING_INTERVAL * 1000);
759
+
760
+ // Reset active subscriptions first
761
+ self._previousSubscriptions = self._activeSubscriptions;
762
+ self._activeSubscriptions = [];
763
+
764
+ // Resubscribe all events
765
+ for (let sub of self._previousSubscriptions) {
766
+ self.subscribe(sub.specification, sub.callback).catch(function (reason) {
767
+ self.log.error(`Resubscribing to ${sub.specification.event_name} failed: ${reason}`);
768
+ });
769
+ }
770
+
771
+ resolve();
772
+ });
773
+
774
+ // CONNECTION CLOSE
775
+ self._socket.addEventListener("close", function (/*event*/) {
776
+ self.log.info("Disconnected from event service");
777
+ clearTimeout(self._pingTimerObj);
778
+ // We try to reconnect if possible
779
+ self._reconnectServer();
780
+ });
781
+
782
+ // CONNECTION ERROR
783
+ self._socket.addEventListener("error", function (error) {
784
+ self.log.error("Websocket connection error");
785
+ // Only reject promise if no connection was made (in that case promise is already resolved)
786
+ if (!was_connected) {
787
+ reject("Unable to connect: " + error);
788
+ }
789
+ });
790
+ });
791
+ }
792
+
793
+ _reconnectServer() {
794
+ if (this._autoReconnect) {
795
+ const retryInterval = Math.floor(this._reconnectRetrySeconds);
796
+ this.log.info(`Reconnecting in ${retryInterval} secs`);
797
+ const self = this;
798
+ setTimeout(function () {
799
+ self._connectServer().then(
800
+ function () {
801
+ self.log.info("Reconnect attempt successful");
802
+ },
803
+ function () {
804
+ self.log.warning("Reconnect attempt failed");
805
+ },
806
+ );
807
+ if (self._reconnectRetrySeconds <= RETRY_MAX_BACKOFF) {
808
+ self._reconnectRetrySeconds = self._reconnectRetrySeconds * 2;
809
+ }
810
+ }, self._reconnectRetrySeconds * 1000);
811
+ } else {
812
+ this.log.info("Auto reconnect disabled (disconnecting?)");
813
+ }
814
+ }
815
+
816
+ _handleIncomingEvent(request) {
817
+ // Find subscription by id
818
+ const subId = request.data.subscription_id;
819
+ for (let sub of this._activeSubscriptions) {
820
+ if (sub.id === subId) {
821
+ this.log.debug(`Received event for subcription id ${sub.id} payload: ` + JSON.stringify(request.data.payload));
822
+ try {
823
+ sub.callback(request.data.payload);
824
+ } catch (e) {
825
+ this.log.warning(`Callback had unhandled exception ${e}`);
826
+ request.resolve();
827
+ }
828
+
829
+ request.resolve();
830
+ return;
831
+ }
832
+ }
833
+
834
+ request.reject(`Invalid subscription id for event: ${subId}`);
835
+ }
836
+ }
837
+
838
+ const isObject$1 = value => typeof value === 'object' && value !== null;
839
+
840
+ // Customized for this use-case
841
+ const isObjectCustom = value =>
842
+ isObject$1(value)
843
+ && !(value instanceof RegExp)
844
+ && !(value instanceof Error)
845
+ && !(value instanceof Date)
846
+ && !(globalThis.Blob && value instanceof globalThis.Blob);
847
+
848
+ const mapObjectSkip = Symbol('mapObjectSkip');
849
+
850
+ const _mapObject = (object, mapper, options, isSeen = new WeakMap()) => {
851
+ options = {
852
+ deep: false,
853
+ target: {},
854
+ ...options,
855
+ };
856
+
857
+ if (isSeen.has(object)) {
858
+ return isSeen.get(object);
859
+ }
860
+
861
+ isSeen.set(object, options.target);
862
+
863
+ const {target} = options;
864
+ delete options.target;
865
+
866
+ const mapArray = array => array.map(element => isObjectCustom(element) ? _mapObject(element, mapper, options, isSeen) : element);
867
+ if (Array.isArray(object)) {
868
+ return mapArray(object);
869
+ }
870
+
871
+ for (const [key, value] of Object.entries(object)) {
872
+ const mapResult = mapper(key, value, object);
873
+
874
+ if (mapResult === mapObjectSkip) {
875
+ continue;
876
+ }
877
+
878
+ let [newKey, newValue, {shouldRecurse = true} = {}] = mapResult;
879
+
880
+ // Drop `__proto__` keys.
881
+ if (newKey === '__proto__') {
882
+ continue;
883
+ }
884
+
885
+ if (options.deep && shouldRecurse && isObjectCustom(newValue)) {
886
+ newValue = Array.isArray(newValue)
887
+ ? mapArray(newValue)
888
+ : _mapObject(newValue, mapper, options, isSeen);
889
+ }
890
+
891
+ target[newKey] = newValue;
892
+ }
893
+
894
+ return target;
895
+ };
896
+
897
+ function mapObject(object, mapper, options) {
898
+ if (!isObject$1(object)) {
899
+ throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`);
900
+ }
901
+
902
+ if (Array.isArray(object)) {
903
+ throw new TypeError('Expected an object, got an array');
904
+ }
905
+
906
+ return _mapObject(object, mapper, options);
907
+ }
908
+
909
+ const UPPERCASE = /[\p{Lu}]/u;
910
+ const LOWERCASE = /[\p{Ll}]/u;
911
+ const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
912
+ const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
913
+ const SEPARATORS = /[_.\- ]+/;
914
+
915
+ const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
916
+ const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, 'gu');
917
+ const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
918
+
919
+ const preserveCamelCase = (string, toLowerCase, toUpperCase, preserveConsecutiveUppercase) => {
920
+ let isLastCharLower = false;
921
+ let isLastCharUpper = false;
922
+ let isLastLastCharUpper = false;
923
+ let isLastLastCharPreserved = false;
924
+
925
+ for (let index = 0; index < string.length; index++) {
926
+ const character = string[index];
927
+ isLastLastCharPreserved = index > 2 ? string[index - 3] === '-' : true;
928
+
929
+ if (isLastCharLower && UPPERCASE.test(character)) {
930
+ string = string.slice(0, index) + '-' + string.slice(index);
931
+ isLastCharLower = false;
932
+ isLastLastCharUpper = isLastCharUpper;
933
+ isLastCharUpper = true;
934
+ index++;
935
+ } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character) && (!isLastLastCharPreserved || preserveConsecutiveUppercase)) {
936
+ string = string.slice(0, index - 1) + '-' + string.slice(index - 1);
937
+ isLastLastCharUpper = isLastCharUpper;
938
+ isLastCharUpper = false;
939
+ isLastCharLower = true;
940
+ } else {
941
+ isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
942
+ isLastLastCharUpper = isLastCharUpper;
943
+ isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
944
+ }
945
+ }
946
+
947
+ return string;
948
+ };
949
+
950
+ const preserveConsecutiveUppercase = (input, toLowerCase) => {
951
+ LEADING_CAPITAL.lastIndex = 0;
952
+
953
+ return input.replaceAll(LEADING_CAPITAL, match => toLowerCase(match));
954
+ };
955
+
956
+ const postProcess = (input, toUpperCase) => {
957
+ SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
958
+ NUMBERS_AND_IDENTIFIER.lastIndex = 0;
959
+
960
+ return input
961
+ .replaceAll(NUMBERS_AND_IDENTIFIER, (match, pattern, offset) => ['_', '-'].includes(input.charAt(offset + match.length)) ? match : toUpperCase(match))
962
+ .replaceAll(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier));
963
+ };
964
+
965
+ function camelCase(input, options) {
966
+ if (!(typeof input === 'string' || Array.isArray(input))) {
967
+ throw new TypeError('Expected the input to be `string | string[]`');
968
+ }
969
+
970
+ options = {
971
+ pascalCase: false,
972
+ preserveConsecutiveUppercase: false,
973
+ ...options,
974
+ };
975
+
976
+ if (Array.isArray(input)) {
977
+ input = input.map(x => x.trim())
978
+ .filter(x => x.length)
979
+ .join('-');
980
+ } else {
981
+ input = input.trim();
982
+ }
983
+
984
+ if (input.length === 0) {
985
+ return '';
986
+ }
987
+
988
+ const toLowerCase = options.locale === false
989
+ ? string => string.toLowerCase()
990
+ : string => string.toLocaleLowerCase(options.locale);
991
+
992
+ const toUpperCase = options.locale === false
993
+ ? string => string.toUpperCase()
994
+ : string => string.toLocaleUpperCase(options.locale);
995
+
996
+ if (input.length === 1) {
997
+ if (SEPARATORS.test(input)) {
998
+ return '';
999
+ }
1000
+
1001
+ return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
1002
+ }
1003
+
1004
+ const hasUpperCase = input !== toLowerCase(input);
1005
+
1006
+ if (hasUpperCase) {
1007
+ input = preserveCamelCase(input, toLowerCase, toUpperCase, options.preserveConsecutiveUppercase);
1008
+ }
1009
+
1010
+ input = input.replace(LEADING_SEPARATORS, '');
1011
+ input = options.preserveConsecutiveUppercase ? preserveConsecutiveUppercase(input, toLowerCase) : toLowerCase(input);
1012
+
1013
+ if (options.pascalCase) {
1014
+ input = toUpperCase(input.charAt(0)) + input.slice(1);
1015
+ }
1016
+
1017
+ return postProcess(input, toUpperCase);
1018
+ }
1019
+
1020
+ class QuickLRU extends Map {
1021
+ #size = 0;
1022
+ #cache = new Map();
1023
+ #oldCache = new Map();
1024
+ #maxSize;
1025
+ #maxAge;
1026
+ #onEviction;
1027
+
1028
+ constructor(options = {}) {
1029
+ super();
1030
+
1031
+ if (!(options.maxSize && options.maxSize > 0)) {
1032
+ throw new TypeError('`maxSize` must be a number greater than 0');
1033
+ }
1034
+
1035
+ if (typeof options.maxAge === 'number' && options.maxAge === 0) {
1036
+ throw new TypeError('`maxAge` must be a number greater than 0');
1037
+ }
1038
+
1039
+ this.#maxSize = options.maxSize;
1040
+ this.#maxAge = options.maxAge || Number.POSITIVE_INFINITY;
1041
+ this.#onEviction = options.onEviction;
1042
+ }
1043
+
1044
+ // For tests.
1045
+ get __oldCache() {
1046
+ return this.#oldCache;
1047
+ }
1048
+
1049
+ #emitEvictions(cache) {
1050
+ if (typeof this.#onEviction !== 'function') {
1051
+ return;
1052
+ }
1053
+
1054
+ for (const [key, item] of cache) {
1055
+ this.#onEviction(key, item.value);
1056
+ }
1057
+ }
1058
+
1059
+ #deleteIfExpired(key, item) {
1060
+ if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
1061
+ if (typeof this.#onEviction === 'function') {
1062
+ this.#onEviction(key, item.value);
1063
+ }
1064
+
1065
+ return this.delete(key);
1066
+ }
1067
+
1068
+ return false;
1069
+ }
1070
+
1071
+ #getOrDeleteIfExpired(key, item) {
1072
+ const deleted = this.#deleteIfExpired(key, item);
1073
+ if (deleted === false) {
1074
+ return item.value;
1075
+ }
1076
+ }
1077
+
1078
+ #getItemValue(key, item) {
1079
+ return item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value;
1080
+ }
1081
+
1082
+ #peek(key, cache) {
1083
+ const item = cache.get(key);
1084
+ return this.#getItemValue(key, item);
1085
+ }
1086
+
1087
+ #set(key, value) {
1088
+ this.#cache.set(key, value);
1089
+ this.#size++;
1090
+
1091
+ if (this.#size >= this.#maxSize) {
1092
+ this.#size = 0;
1093
+ this.#emitEvictions(this.#oldCache);
1094
+ this.#oldCache = this.#cache;
1095
+ this.#cache = new Map();
1096
+ }
1097
+ }
1098
+
1099
+ #moveToRecent(key, item) {
1100
+ this.#oldCache.delete(key);
1101
+ this.#set(key, item);
1102
+ }
1103
+
1104
+ * #entriesAscending() {
1105
+ for (const item of this.#oldCache) {
1106
+ const [key, value] = item;
1107
+ if (!this.#cache.has(key)) {
1108
+ const deleted = this.#deleteIfExpired(key, value);
1109
+ if (deleted === false) {
1110
+ yield item;
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ for (const item of this.#cache) {
1116
+ const [key, value] = item;
1117
+ const deleted = this.#deleteIfExpired(key, value);
1118
+ if (deleted === false) {
1119
+ yield item;
1120
+ }
1121
+ }
1122
+ }
1123
+
1124
+ get(key) {
1125
+ if (this.#cache.has(key)) {
1126
+ const item = this.#cache.get(key);
1127
+ return this.#getItemValue(key, item);
1128
+ }
1129
+
1130
+ if (this.#oldCache.has(key)) {
1131
+ const item = this.#oldCache.get(key);
1132
+ if (this.#deleteIfExpired(key, item) === false) {
1133
+ this.#moveToRecent(key, item);
1134
+ return item.value;
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ set(key, value, {maxAge = this.#maxAge} = {}) {
1140
+ const expiry = typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY
1141
+ ? (Date.now() + maxAge)
1142
+ : undefined;
1143
+
1144
+ if (this.#cache.has(key)) {
1145
+ this.#cache.set(key, {
1146
+ value,
1147
+ expiry,
1148
+ });
1149
+ } else {
1150
+ this.#set(key, {value, expiry});
1151
+ }
1152
+
1153
+ return this;
1154
+ }
1155
+
1156
+ has(key) {
1157
+ if (this.#cache.has(key)) {
1158
+ return !this.#deleteIfExpired(key, this.#cache.get(key));
1159
+ }
1160
+
1161
+ if (this.#oldCache.has(key)) {
1162
+ return !this.#deleteIfExpired(key, this.#oldCache.get(key));
1163
+ }
1164
+
1165
+ return false;
1166
+ }
1167
+
1168
+ peek(key) {
1169
+ if (this.#cache.has(key)) {
1170
+ return this.#peek(key, this.#cache);
1171
+ }
1172
+
1173
+ if (this.#oldCache.has(key)) {
1174
+ return this.#peek(key, this.#oldCache);
1175
+ }
1176
+ }
1177
+
1178
+ expiresIn(key) {
1179
+ const item = this.#cache.get(key) ?? this.#oldCache.get(key);
1180
+ if (item) {
1181
+ return item.expiry ? item.expiry - Date.now() : Number.POSITIVE_INFINITY;
1182
+ }
1183
+ }
1184
+
1185
+ delete(key) {
1186
+ const deleted = this.#cache.delete(key);
1187
+ if (deleted) {
1188
+ this.#size--;
1189
+ }
1190
+
1191
+ return this.#oldCache.delete(key) || deleted;
1192
+ }
1193
+
1194
+ clear() {
1195
+ this.#cache.clear();
1196
+ this.#oldCache.clear();
1197
+ this.#size = 0;
1198
+ }
1199
+
1200
+ resize(newSize) {
1201
+ if (!(newSize && newSize > 0)) {
1202
+ throw new TypeError('`maxSize` must be a number greater than 0');
1203
+ }
1204
+
1205
+ const items = [...this.#entriesAscending()];
1206
+ const removeCount = items.length - newSize;
1207
+ if (removeCount < 0) {
1208
+ this.#cache = new Map(items);
1209
+ this.#oldCache = new Map();
1210
+ this.#size = items.length;
1211
+ } else {
1212
+ if (removeCount > 0) {
1213
+ this.#emitEvictions(items.slice(0, removeCount));
1214
+ }
1215
+
1216
+ this.#oldCache = new Map(items.slice(removeCount));
1217
+ this.#cache = new Map();
1218
+ this.#size = 0;
1219
+ }
1220
+
1221
+ this.#maxSize = newSize;
1222
+ }
1223
+
1224
+ evict(count = 1) {
1225
+ const requested = Number(count);
1226
+ if (!requested || requested <= 0) {
1227
+ return;
1228
+ }
1229
+
1230
+ const items = [...this.#entriesAscending()];
1231
+ const evictCount = Math.trunc(Math.min(requested, Math.max(items.length - 1, 0)));
1232
+ if (evictCount <= 0) {
1233
+ return;
1234
+ }
1235
+
1236
+ this.#emitEvictions(items.slice(0, evictCount));
1237
+ this.#oldCache = new Map(items.slice(evictCount));
1238
+ this.#cache = new Map();
1239
+ this.#size = 0;
1240
+ }
1241
+
1242
+ * keys() {
1243
+ for (const [key] of this) {
1244
+ yield key;
1245
+ }
1246
+ }
1247
+
1248
+ * values() {
1249
+ for (const [, value] of this) {
1250
+ yield value;
1251
+ }
1252
+ }
1253
+
1254
+ * [Symbol.iterator]() {
1255
+ for (const item of this.#cache) {
1256
+ const [key, value] = item;
1257
+ const deleted = this.#deleteIfExpired(key, value);
1258
+ if (deleted === false) {
1259
+ yield [key, value.value];
1260
+ }
1261
+ }
1262
+
1263
+ for (const item of this.#oldCache) {
1264
+ const [key, value] = item;
1265
+ if (!this.#cache.has(key)) {
1266
+ const deleted = this.#deleteIfExpired(key, value);
1267
+ if (deleted === false) {
1268
+ yield [key, value.value];
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ * entriesDescending() {
1275
+ let items = [...this.#cache];
1276
+ for (let i = items.length - 1; i >= 0; --i) {
1277
+ const item = items[i];
1278
+ const [key, value] = item;
1279
+ const deleted = this.#deleteIfExpired(key, value);
1280
+ if (deleted === false) {
1281
+ yield [key, value.value];
1282
+ }
1283
+ }
1284
+
1285
+ items = [...this.#oldCache];
1286
+ for (let i = items.length - 1; i >= 0; --i) {
1287
+ const item = items[i];
1288
+ const [key, value] = item;
1289
+ if (!this.#cache.has(key)) {
1290
+ const deleted = this.#deleteIfExpired(key, value);
1291
+ if (deleted === false) {
1292
+ yield [key, value.value];
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ * entriesAscending() {
1299
+ for (const [key, value] of this.#entriesAscending()) {
1300
+ yield [key, value.value];
1301
+ }
1302
+ }
1303
+
1304
+ get size() {
1305
+ if (!this.#size) {
1306
+ return this.#oldCache.size;
1307
+ }
1308
+
1309
+ let oldCacheSize = 0;
1310
+ for (const key of this.#oldCache.keys()) {
1311
+ if (!this.#cache.has(key)) {
1312
+ oldCacheSize++;
1313
+ }
1314
+ }
1315
+
1316
+ return Math.min(this.#size + oldCacheSize, this.#maxSize);
1317
+ }
1318
+
1319
+ get maxSize() {
1320
+ return this.#maxSize;
1321
+ }
1322
+
1323
+ get maxAge() {
1324
+ return this.#maxAge;
1325
+ }
1326
+
1327
+ entries() {
1328
+ return this.entriesAscending();
1329
+ }
1330
+
1331
+ forEach(callbackFunction, thisArgument = this) {
1332
+ for (const [key, value] of this.entriesAscending()) {
1333
+ callbackFunction.call(thisArgument, value, key, this);
1334
+ }
1335
+ }
1336
+
1337
+ get [Symbol.toStringTag]() {
1338
+ return 'QuickLRU';
1339
+ }
1340
+
1341
+ toString() {
1342
+ return `QuickLRU(${this.size}/${this.maxSize})`;
1343
+ }
1344
+
1345
+ [Symbol.for('nodejs.util.inspect.custom')]() {
1346
+ return this.toString();
1347
+ }
1348
+ }
1349
+
1350
+ const has = (array, key) => array.some(element => {
1351
+ if (typeof element === 'string') {
1352
+ return element === key;
1353
+ }
1354
+
1355
+ element.lastIndex = 0;
1356
+
1357
+ return element.test(key);
1358
+ });
1359
+
1360
+ // Preserve numeric keys. The trim check excludes empty/whitespace strings,
1361
+ // which Number() would incorrectly convert to 0.
1362
+ const isNumericKey = key => key.trim() !== '' && !Number.isNaN(Number(key));
1363
+
1364
+ const cache = new QuickLRU({maxSize: 100_000});
1365
+
1366
+ const isBuiltIn = value =>
1367
+ ArrayBuffer.isView(value)
1368
+ // Check for known built-in types
1369
+ || value instanceof Date
1370
+ || value instanceof RegExp
1371
+ || value instanceof Error
1372
+ || value instanceof Map
1373
+ || value instanceof Set
1374
+ || value instanceof WeakMap
1375
+ || value instanceof WeakSet
1376
+ || value instanceof Promise;
1377
+
1378
+ // Reproduces behavior from `map-obj`.
1379
+ const isObject = value =>
1380
+ typeof value === 'object'
1381
+ && value !== null
1382
+ && !isBuiltIn(value);
1383
+
1384
+ const transform = (input, options = {}, isSeen = new WeakMap(), parentPath) => {
1385
+ if (!isObject(input)) {
1386
+ return input;
1387
+ }
1388
+
1389
+ // Check for circular references
1390
+ if (isSeen.has(input)) {
1391
+ return isSeen.get(input);
1392
+ }
1393
+
1394
+ const {
1395
+ exclude,
1396
+ pascalCase = false,
1397
+ stopPaths,
1398
+ deep = false,
1399
+ preserveConsecutiveUppercase = false,
1400
+ } = options;
1401
+
1402
+ const stopPathsSet = new Set(stopPaths);
1403
+
1404
+ // Handle arrays directly
1405
+ if (Array.isArray(input)) {
1406
+ const result = [];
1407
+ isSeen.set(input, result);
1408
+
1409
+ for (const item of input) {
1410
+ result.push(isObject(item) ? transform(item, options, isSeen, parentPath) : item);
1411
+ }
1412
+
1413
+ return result;
1414
+ }
1415
+
1416
+ // Pre-allocate the result object for circular reference handling
1417
+ const result = {};
1418
+ isSeen.set(input, result);
1419
+
1420
+ const makeMapper = currentParentPath => (key, value) => {
1421
+ // Handle deep transformation
1422
+ if (deep && isObject(value)) {
1423
+ const path = currentParentPath === undefined ? key : `${currentParentPath}.${key}`;
1424
+
1425
+ if (!stopPathsSet.has(path)) {
1426
+ // Handle arrays and objects recursively
1427
+ value = Array.isArray(value)
1428
+ ? value.map(item => isObject(item) ? transform(item, options, isSeen, path) : item)
1429
+ : transform(value, options, isSeen, path);
1430
+ }
1431
+ }
1432
+
1433
+ // Skip transformation for excluded keys, numeric keys
1434
+ // Only transform string keys (preserve symbols and numbers)
1435
+ if (typeof key === 'string' && !isNumericKey(key) && !(exclude && has(exclude, key))) {
1436
+ const cacheKey = pascalCase ? `${key}_` : key;
1437
+
1438
+ if (cache.has(cacheKey)) {
1439
+ key = cache.get(cacheKey);
1440
+ } else {
1441
+ // Preserve leading `_` and `$` as they typically have semantic meaning
1442
+ // This should eventually be fixed in the `camelcase` package itself
1443
+ const leadingPrefix = key.match(/^[_$]*/)[0];
1444
+ const transformed = camelCase(key.slice(leadingPrefix.length), {pascalCase, locale: false, preserveConsecutiveUppercase});
1445
+ key = leadingPrefix + transformed;
1446
+
1447
+ // Only cache reasonable length keys to prevent memory abuse
1448
+ if (key.length < 100) {
1449
+ cache.set(cacheKey, key);
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ return [key, value];
1455
+ };
1456
+
1457
+ const mappedResult = mapObject(input, makeMapper(parentPath), {deep: false});
1458
+
1459
+ // Copy properties to the pre-allocated result for circular reference handling
1460
+ Object.assign(result, mappedResult);
1461
+
1462
+ // Preserve symbol keys (mapObject doesn't handle them)
1463
+ const symbols = Object.getOwnPropertySymbols(input);
1464
+ for (const symbol of symbols) {
1465
+ result[symbol] = deep && isObject(input[symbol])
1466
+ ? transform(input[symbol], options, isSeen, parentPath)
1467
+ : input[symbol];
1468
+ }
1469
+
1470
+ return result;
1471
+ };
1472
+
1473
+ function camelcaseKeys(input, options) {
1474
+ const isSeen = new WeakMap();
1475
+
1476
+ if (Array.isArray(input)) {
1477
+ // More efficient array handling - directly map the array
1478
+ return input.map(item =>
1479
+ isObject(item)
1480
+ ? transform(item, options, isSeen, undefined)
1481
+ : item);
1482
+ }
1483
+
1484
+ return transform(input, options, isSeen);
1485
+ }
1486
+
1487
+ // 'use strict';
1488
+
1489
+ const options = {
1490
+ wssServer: "wss://api.telegenta.com/sip/",
1491
+ eventServer: "wss://api.telegenta.com/es/",
1492
+ provisionToken: null,
1493
+ authorizationUsername: null,
1494
+ password: null,
1495
+ callToken: null,
1496
+ register: true,
1497
+ iceGatherTimeout: 3000,
1498
+ playSignalTones: true,
1499
+ logger: null,
1500
+ logLevel: 3,
1501
+ connectEventService: false,
1502
+ useCallDataInfoEvent: false,
1503
+ toneVolume: 0.3,
1504
+ autoBusyIncoming: true,
1505
+ autoRemoveSessionTime: 3000,
1506
+ playRingOnIncoming: true,
1507
+ playCallInitializedTone: false,
1508
+ };
1509
+
1510
+ // DTMF Frequency table
1511
+ const DTMF_FREQUENCY_TABLE = {
1512
+ 1: [1209, 697],
1513
+ 2: [1336, 697],
1514
+ 3: [1477, 697],
1515
+ a: [1633, 697],
1516
+ 4: [1209, 770],
1517
+ 5: [1336, 770],
1518
+ 6: [1477, 770],
1519
+ b: [1633, 770],
1520
+ 7: [1209, 852],
1521
+ 8: [1336, 852],
1522
+ 9: [1477, 852],
1523
+ c: [1633, 852],
1524
+ "*": [1209, 941],
1525
+ 0: [1336, 941],
1526
+ "#": [1477, 941],
1527
+ d: [1633, 941],
1528
+ };
1529
+
1530
+ // SIGNAL TONE SEQUENCES
1531
+
1532
+ // Params: d = Duration, f1 = Frequency 1, f2 = Frequency 2, g = Gap
1533
+ const ST_TEMP_UNAVAIL = [{ d: 250, f1: 425, f2: 0, g: 250 }];
1534
+ const ST_BUSY = [{ d: 500, f1: 425, f2: 0, g: 500 }];
1535
+ const ST_INVALID_NUMBER = [
1536
+ { d: 333, f1: 950, f2: 0, g: 0 },
1537
+ { d: 333, f1: 1400, f2: 0, g: 0 },
1538
+ { d: 333, f1: 1800, f2: 0, g: 1000 },
1539
+ ];
1540
+ const ST_CALLINIT = [
1541
+ { d: 200, f1: 500, f2: 0, g: 0 },
1542
+ { d: 200, f1: 700, f2: 0, g: 5000 },
1543
+ ];
1544
+ const ST_RINGBACK = [{ d: 1000, f1: 425, f2: 0, g: 4000 }];
1545
+ const ST_INCOMING = [
1546
+ { d: 150, f1: 400, f2: 0, g: 0 },
1547
+ { d: 150, f1: 600, f2: 0, g: 0 },
1548
+ { d: 150, f1: 400, f2: 0, g: 0 },
1549
+ { d: 150, f1: 600, f2: 0, g: 0 },
1550
+ { d: 150, f1: 400, f2: 0, g: 0 },
1551
+ { d: 150, f1: 600, f2: 0, g: 3000 },
1552
+ ];
1553
+
1554
+ /**
1555
+ * This class acts as a SIP user Agent and is responsible for handling calls using the WebRTC interface in the browser.<br>
1556
+ * If you have your own log handler or if you want to adjust the log level you can create your own logging instance, when you create the Phone
1557
+ * instance. See {@link LogHandler} class for more information.
1558
+ *
1559
+ * @param {LogHandler} [logger] - Custom log instance
1560
+ *
1561
+ * @fires connectionStateChange on connection state changes.
1562
+ * @fires sessionCreated When a new session has been created
1563
+ * @fires sessionRemoved When a existing session has been removed (after autoRemoveSessionTime)
1564
+ * @fires callDataInfo Receive detailed information about call after call has been terminated
1565
+ * @fires incomingCall Incoming call sessions are received on this event
1566
+ *
1567
+ * @example
1568
+ * // Creating Phone object and use it make a call
1569
+ * const phone = new Telegenta.Phone()
1570
+ * phone.prepareMedia().then(() => {
1571
+ * // Hook up events
1572
+ * ...
1573
+ * // Start phone
1574
+ * phone.start(options);
1575
+ *
1576
+ * // Dial 12345678
1577
+ * session = phone.call('12345678');
1578
+ * }
1579
+ */
1580
+ class Phone extends EventEmitter {
1581
+ constructor(logger = null) {
1582
+ super();
1583
+
1584
+ this._mediaPermission = false;
1585
+ this._mediaPermissionError = false;
1586
+ this._UA = null;
1587
+ this._connectionState = null;
1588
+ this._isStarted = false;
1589
+ this._eventClient = null;
1590
+ this._options = null;
1591
+
1592
+ // List of active sessions
1593
+ this._sessionCounter = 0;
1594
+ this._activeSessions = {};
1595
+
1596
+ // Create remote audio element
1597
+ this._remoteAudioElement = document.createElement("AUDIO");
1598
+ this._remoteAudioElement.autoplay = true;
1599
+
1600
+ // Tone generator
1601
+ this._toneAudioElement = document.createElement("AUDIO");
1602
+ this._toneAudioElement.autoplay = true;
1603
+ this._toneTimer = null;
1604
+ this._repeatToneActive = false;
1605
+ this._repeatToneSequence = [];
1606
+ this._repeatToneSequencePos = 0;
1607
+ this._toneGainProcessor = null;
1608
+ this._oscillator1 = null;
1609
+ this._oscillator2 = null;
1610
+
1611
+ // Set default logger
1612
+ this.log = logger ? logger : new LogHandler(LOGLVL$1.INFO);
1613
+
1614
+ // Handle jssip debug logs
1615
+ if (this.log.level === LOGLVL$1.DEBUG) {
1616
+ JsSIP.debug.disable("JsSIP:*");
1617
+ } else {
1618
+ JsSIP.debug.disable("JsSIP:*");
1619
+ }
1620
+ }
1621
+
1622
+ /**
1623
+ * Prepare media and get media permission from browser
1624
+ * This must be done prior to using the phone or getting device lists.
1625
+ *
1626
+ * @return A promise which will be resolved when user accepts access to media devices
1627
+ *
1628
+ */
1629
+ prepareMedia() {
1630
+ this.log.info("Get media permission");
1631
+ const self = this;
1632
+ const constraints = { audio: true, video: false };
1633
+ return new Promise(function (resolve, reject) {
1634
+ navigator.mediaDevices.getUserMedia(constraints).then(
1635
+ () => {
1636
+ self._mediaPermission = true;
1637
+ self._mediaPermissionError = false;
1638
+ self.log.info("Media permission granted");
1639
+ resolve();
1640
+ },
1641
+ (err) => {
1642
+ self._mediaPermission = false;
1643
+ self._mediaPermissionError = true;
1644
+ self.log.error("Denied media permission: " + err);
1645
+ reject(err);
1646
+ },
1647
+ );
1648
+ });
1649
+ }
1650
+
1651
+ /**
1652
+ * Start phone and connect to server if needed
1653
+ *
1654
+ * @param phoneOptions {object} configuration options
1655
+ * @param {string} [phoneOptions.provisionToken] Provisioning token from API service
1656
+ * @param {string} [phoneOptions.authorizationUsername] Authorizing username
1657
+ * @param {string} [phoneOptions.password] WSS Password
1658
+ * @param {string} [phoneOptions.callToken] Call token
1659
+ * @param {boolean} [phoneOptions.connectEventService=false] Connect to event service (required to send call commands and receive callDataInfo event)
1660
+ * @param {boolean} [phoneOptions.useCallDataInfoEvent=false] Subscribe to callDataInfo remote event
1661
+ * @param {boolean} [phoneOptions.register=true] Register on server to receive enable incoming calls
1662
+ * @param {number} [phoneOptions.playSignalTones=false] Play signal tones (busy, invalid number, etc) remotely
1663
+ * @param {number} [phoneOptions.iceGatherTimeout=3000] Number of millisecs before ICE gather will be forced stopped and candidate selection will take place
1664
+ * @param {number} [phoneOptions.toneVolume=0.3] Signal- and DTMF tone volume (0-1)
1665
+ * @param {boolean} [phoneOptions.autoBusyIncoming=true] Automatically reject calls with busy if active session exists
1666
+ * @param {number} [phoneOptions.autoRemoveSessionTime=3000] Automatically remove session after interval specified
1667
+ * @param {number} [phoneOptions.playRingOnIncoming=true] Play ringing tones on incoming calls
1668
+ * @param {number} [phoneOptions.playCallInitializedTone=false] Play tone when call has been initiated (before progress/ringback tones)
1669
+ *
1670
+ * @throws MediaNotPrepared - If {@link prepareMedia} was not called and resolved prior to calling this method
1671
+ * @throws InvalidOptions - If one or more options are invalid
1672
+ * @throws UnableToConnect - If unable to connect phone to server
1673
+ */
1674
+
1675
+ start(phoneOptions) {
1676
+ this._checkMediaPermission();
1677
+ // Check phone is not running already, if so stop it first
1678
+ if (this._isStarted) {
1679
+ this.log.warning("Phone already started, stopping first");
1680
+ this.stop();
1681
+ }
1682
+
1683
+ // Apply options
1684
+ this._options = Object.assign(options, phoneOptions);
1685
+
1686
+ this._initToneGenerator();
1687
+
1688
+ // Unpack provisioning token if specified
1689
+ if (this._options.provisionToken) {
1690
+ let pt = null;
1691
+ try {
1692
+ pt = JSON.parse(atob(this._options.provisionToken));
1693
+ } catch (err) {
1694
+ throw new InvalidOptions("provisionToken", "Invalid token");
1695
+ }
1696
+
1697
+ this.log.info("Using provisioning token authorization");
1698
+ this._options.wssServer = pt.wss_server;
1699
+ this._options.eventServer = pt.event_server;
1700
+ this._options.authorizationUsername = pt.wss_username;
1701
+ this._options.password = pt.wss_password;
1702
+ this._options.callToken = pt.wss_token;
1703
+ } else {
1704
+ this.log.info("Using manual specified credentials");
1705
+
1706
+ // Check auth info is provided
1707
+ if (!this._options.authorizationUsername) {
1708
+ throw new InvalidOptions("authorizationUsername", "Missing username");
1709
+ }
1710
+
1711
+ if (!this._options.password) {
1712
+ throw new InvalidOptions("password", "Missing password");
1713
+ }
1714
+
1715
+ if (!this._options.callToken) {
1716
+ throw new InvalidOptions("callToken", "Missing call token");
1717
+ }
1718
+ }
1719
+
1720
+ // If we must subscribe to call data info event, we enable event service
1721
+ if (this._options.useCallDataInfoEvent && !this._options.connectEventService) {
1722
+ this.log.info(`Enabled event service because 'useCallDataInfoEvent' is enabled`);
1723
+ this._options.connectEventService = true;
1724
+ }
1725
+
1726
+ // Connect to event service if needed
1727
+ if (this._options.connectEventService) {
1728
+ // Create and connect event client
1729
+ this._eventClient = new EventClient(this.log);
1730
+
1731
+ this._eventClient.connect(this._options.eventServer).then(
1732
+ () => {
1733
+ this.log.info(`Connected to event server`);
1734
+
1735
+ // Subscript to CallDataEvent is needed
1736
+ if (this._options.useCallDataInfoEvent) {
1737
+ // Subscribe to calldata_available events
1738
+ let eventSpec = {
1739
+ event_name: "calldata_available",
1740
+ call_token: this._options.callToken,
1741
+ };
1742
+
1743
+ this._eventClient
1744
+ .subscribe(eventSpec, (data) => {
1745
+ try {
1746
+ this.log.debug("Received remote call data info");
1747
+ this.log.debug(data);
1748
+ this._fireCallDataInfoEvent(data);
1749
+ } catch (e) {
1750
+ this.log.error("Event callback failed: " + e);
1751
+ }
1752
+ })
1753
+ .then(
1754
+ (subscriptionId) => {
1755
+ this.log.info(`Subscription successful ID = ${subscriptionId}`);
1756
+ },
1757
+ (reason) => {
1758
+ this.log.error(`Subscribe failed: ${reason}`);
1759
+ },
1760
+ );
1761
+ }
1762
+ },
1763
+ (reason) => {
1764
+ this.log.error(`Unable to connect to event server: ${reason}`);
1765
+ },
1766
+ );
1767
+ }
1768
+
1769
+ let wsSocket = null;
1770
+ try {
1771
+ wsSocket = new JsSIP.WebSocketInterface(this._options.wssServer);
1772
+ } catch (err) {
1773
+ throw new UnableToConnect("Unable to connect phone websocket: " + err);
1774
+ }
1775
+
1776
+ this.log.info("Starting phone");
1777
+ this.log.debug(this._options);
1778
+
1779
+ this._UA = new JsSIP.UA({
1780
+ sockets: [wsSocket],
1781
+ authorization_user: this._options.authorizationUsername,
1782
+ password: this._options.password,
1783
+ register: this._options.register,
1784
+ display_name: this._options.authorizationUsername,
1785
+ uri: this._options.authorizationUsername + "@telegenta.com",
1786
+ session_timers: false,
1787
+ });
1788
+
1789
+ // Setup remote media handler
1790
+ const self = this;
1791
+ this._UA.on("newRTCSession", function (data) {
1792
+ self.log.debug("New RTCSession");
1793
+ if (data.originator === "remote") {
1794
+ // Create new session
1795
+ const session = new Session(self);
1796
+
1797
+ session.newInboundSession(data);
1798
+
1799
+ if (self._options.playRingOnIncoming) {
1800
+ self._startRepeatTone(ST_INCOMING);
1801
+ }
1802
+
1803
+ // Check if we have other sessions and reject call if autoBusyIncoming is true
1804
+ if (self._options.autoBusyIncoming) {
1805
+ if (Object.keys(self._activeSessions).length > 0) {
1806
+ self.log.info(`Autorejecting incoming call ${session.uuid} because there is already active calls`);
1807
+ session.reject(486, "Busy");
1808
+ return;
1809
+ } else {
1810
+ self.log.debug("There are no other sessions, adding incoming call");
1811
+ }
1812
+ }
1813
+
1814
+ self._addSession(session);
1815
+
1816
+ // Set new session state
1817
+ session._setSessionState("INCOMING");
1818
+
1819
+ // Now fire incomingCall event
1820
+ self._fireIncomingCallEvent(session);
1821
+ }
1822
+ });
1823
+
1824
+ // Setup connection state event subscriptions
1825
+ this._UA.on("connecting", function (/* data */) {
1826
+ self._setConnectionState("CONNECTING");
1827
+ });
1828
+
1829
+ this._UA.on("connected", function (/* data */) {
1830
+ self._setConnectionState("CONNECTED");
1831
+ });
1832
+
1833
+ this._UA.on("disconnected", function (/* data */) {
1834
+ self._setConnectionState("DISCONNECTED");
1835
+ });
1836
+
1837
+ this._UA.on("registered", function (/* data */) {
1838
+ self._setConnectionState("REGISTERED");
1839
+ });
1840
+
1841
+ this._UA.on("unregistered", function (/* data */) {
1842
+ self._setConnectionState("UNREGISTERED");
1843
+ });
1844
+
1845
+ this._UA.on("registrationFailed", function (/* data */) {
1846
+ self._setConnectionState("REGISTRATION_FAILED");
1847
+ });
1848
+
1849
+ this._UA.start();
1850
+ this._isStarted = true;
1851
+ this.log.info("Successfully started phone");
1852
+ }
1853
+
1854
+ /**
1855
+ * Stops the phone client, gracefully disconnects from the server and terminates any active calls
1856
+ * @throws NotReady - If phone is not started
1857
+ */
1858
+ stop() {
1859
+ // Check phone is started first
1860
+ if (!this._isStarted) {
1861
+ throw new NotReady("Phone is not started");
1862
+ }
1863
+
1864
+ // Make sure events are not firing anymore, we can't rely on GC.
1865
+ this._UA.removeAllListeners();
1866
+
1867
+ // Stop the UA
1868
+ this._UA.stop();
1869
+ delete this._UA;
1870
+
1871
+ // Disconnect event client if needed
1872
+ if (!this._eventClient) {
1873
+ this.log.debug("Event client not running, not stopping");
1874
+ return;
1875
+ }
1876
+ try {
1877
+ this._eventClient.disconnect();
1878
+ delete this._eventClient;
1879
+ } catch (e) {
1880
+ this.log.warning("Unable to disconnect event client");
1881
+ }
1882
+
1883
+ this._isStarted = false;
1884
+ this.log.info("Phone client successfully stopped");
1885
+ }
1886
+
1887
+ /**
1888
+ * Start a new call
1889
+ * If you have other active calls they will be set on hold
1890
+ * @param number Number to call
1891
+ * @param params {object} call options
1892
+ * @param {number} [params.record] Recording retention days. The number of days recording will be kept. Setting this to 0 disables recording.
1893
+ * @param {number} [params.explicitCallerId] Force a specific caller id, by providing explicit phone number id
1894
+ * @param {number} [params.explicitShortCallerId] Force a specific caller id, by providing explicit short caller id
1895
+ * @param {number} [params.answerCallTimeout=0] Hangup call with NO ANSWER if call is not picked up before specified interval (seconds). If 0, timeout is disabled.
1896
+ * @param {number} [params.maximumCallCost] Maximum call cost (in customer currency) allowed for call. The call will automatically be stopped when the specified cost has been reached. Setting this to 0 disables checking for maximum cost.
1897
+ * @param {string} [params.metaInfo] Add meta info to call, which will be available in server events and inside call complete webhook (max 256 chars)
1898
+ * @return {Session} object for call
1899
+ * @throws NotReady - If phone is not ready to call
1900
+ * @throws NotAllowed - If there are calls currently connecting
1901
+ *
1902
+ */
1903
+ call(number, params = {}) {
1904
+ // Check phone is started first
1905
+ if (!this._isStarted) {
1906
+ throw new NotReady("Phone is not started");
1907
+ }
1908
+
1909
+ // Check we dont have any other connecting calls
1910
+ if (this._checkForConnectingCalls()) {
1911
+ throw NotAllowed("Unable to make new calls while others are connecting");
1912
+ }
1913
+
1914
+ // Hold other calls if needed
1915
+ this.holdAllCalls();
1916
+
1917
+ const callParams = Object.assign(
1918
+ {
1919
+ record: false,
1920
+ maximumCallCost: 0,
1921
+ answerCallTimeout: 0,
1922
+ explicitCallerId: null,
1923
+ explicitShortCallerId: null,
1924
+ metaInfo: null,
1925
+ _isListenCall: false,
1926
+ },
1927
+ params,
1928
+ );
1929
+
1930
+ // Trim phone number to remove spaces and '()'
1931
+ number = number.trim().replace(/ |\(|\)/g, "");
1932
+
1933
+ this.log.info("Calling number: " + number);
1934
+
1935
+ let session = new Session(this);
1936
+ session.newOutboundSession(number, callParams);
1937
+ this._addSession(session);
1938
+ return session;
1939
+ }
1940
+
1941
+ /**
1942
+ * Start a new session and listen to another call
1943
+ * @param listenKey provided by API endpoint "active-calls" and the event library subscription for resource name "activecall"
1944
+ * @return {Session} object for listen
1945
+ * @throws NotReady - If phone is not ready to call
1946
+ * @throws NotAllowed - If there are calls currently connecting
1947
+ */
1948
+ listen(listenKey) {
1949
+ const s = listenKey.split("@");
1950
+ let callUUID = s[0];
1951
+ let mediaSwitch = "sip:" + s[1];
1952
+
1953
+ this.log.debug("Initiating listen to call with uuid " + callUUID + " on media switch " + mediaSwitch);
1954
+ return this.call("listen-" + callUUID, {
1955
+ mediaSwitchURI: mediaSwitch,
1956
+ _isListenCall: true,
1957
+ });
1958
+ }
1959
+
1960
+ /**
1961
+ * Get session by UUID, if none are found null is returned
1962
+ * @param uuid
1963
+ * @returns {Session|null}
1964
+ */
1965
+ getSessionByUUID(uuid) {
1966
+ for (let key in this._activeSessions) {
1967
+ if (this._activeSessions.hasOwnProperty(key)) {
1968
+ if (this._activeSessions[key].uuid === uuid) {
1969
+ return this._activeSessions[key];
1970
+ }
1971
+ }
1972
+ }
1973
+
1974
+ return null;
1975
+ }
1976
+
1977
+ /**
1978
+ * Return phone options
1979
+ * @returns {phoneOptions}
1980
+ */
1981
+ get options() {
1982
+ return this._options;
1983
+ }
1984
+
1985
+ /**
1986
+ * Return list of current sessions
1987
+ * @returns [Session]
1988
+ */
1989
+ get sessions() {
1990
+ let s = [];
1991
+ for (let key in this._activeSessions) {
1992
+ if (this._activeSessions.hasOwnProperty(key)) {
1993
+ s.push(this._activeSessions[key]);
1994
+ }
1995
+ }
1996
+
1997
+ return s;
1998
+ }
1999
+
2000
+ /**
2001
+ * Return media permission state
2002
+ * @return {boolean}
2003
+ */
2004
+ get hasMediaPermission() {
2005
+ return this._mediaPermission;
2006
+ }
2007
+
2008
+ /**
2009
+ * Media permission denied
2010
+ * @return {boolean}
2011
+ */
2012
+ get hasMediaPermissionError() {
2013
+ return this._mediaPermissionError;
2014
+ }
2015
+
2016
+ /**
2017
+ * Phone client started
2018
+ * @return {boolean}
2019
+ */
2020
+ get isStarted() {
2021
+ return this._isStarted;
2022
+ }
2023
+
2024
+ /**
2025
+ * Verify browser is supported
2026
+ * @return true if browser supported
2027
+ */
2028
+ get browserSupported() {
2029
+ return browserIsChrome() || browserIsFirefox() || browserIsOpera() || browserIsSafari();
2030
+ }
2031
+
2032
+ /**
2033
+ * Remote audio HTML element
2034
+ */
2035
+ get remoteAudioElement() {
2036
+ return this._remoteAudioElement;
2037
+ }
2038
+
2039
+ // TONE STUFF
2040
+ /**
2041
+ * Play tone locally (on incoming remote audio)
2042
+ * @param duration Duration of tone in ms
2043
+ * @param freq1 Frequency in Hz of tone 1
2044
+ * @param freq2 Frequency in Hz of tone 2
2045
+ * @param callback Function to be called after tone has ended
2046
+ * @throws NotAllowed - If audio api is not supported by browser
2047
+ */
2048
+ playTone(duration, freq1, freq2 = 0, callback = null) {
2049
+ this.log.debug(`Playing tone ${freq1} / ${freq2} for ${duration} ms`);
2050
+
2051
+ this._oscillator1.frequency.value = freq1;
2052
+ this._oscillator2.frequency.value = freq2;
2053
+ this._toneGainProcessor.gain.value = this._options.toneVolume;
2054
+
2055
+ // Set timer to stop tone again
2056
+ const self = this;
2057
+ this._toneTimer = setTimeout(function () {
2058
+ self.stopTone();
2059
+
2060
+ // Handle callback now
2061
+ if (callback) {
2062
+ callback();
2063
+ }
2064
+ }, duration);
2065
+ }
2066
+
2067
+ /**
2068
+ * Immediately stop playing tones
2069
+ * (Tone callbacks will be cancelled)
2070
+ */
2071
+ stopTone() {
2072
+ this._oscillator1.frequency.value = 0;
2073
+ this._oscillator2.frequency.value = 0;
2074
+ this._toneGainProcessor.gain.value = 0;
2075
+ clearTimeout(this._toneTimer);
2076
+ }
2077
+
2078
+ // Begin repeating tones
2079
+ _startRepeatTone(sequences) {
2080
+ this.log.info("Repeating tone sequence started");
2081
+ this.log.debug(sequences);
2082
+ this._repeatToneSequence = sequences;
2083
+ this._repeatToneSequencePos = 0;
2084
+ this._repeatToneActive = true;
2085
+ this._doRepeatTone();
2086
+ }
2087
+
2088
+ // Do one tone
2089
+ _doRepeatTone() {
2090
+ if (this._repeatToneActive) {
2091
+ this.log.debug(`Playing repeated tone, pos = ${this._repeatToneSequencePos}`);
2092
+
2093
+ // Get tone definitions and move to next sequence position
2094
+ let toneDef = this._repeatToneSequence[this._repeatToneSequencePos];
2095
+ this._repeatToneSequencePos++;
2096
+ if (this._repeatToneSequencePos === this._repeatToneSequence.length) {
2097
+ this._repeatToneSequencePos = 0;
2098
+ }
2099
+
2100
+ const self = this;
2101
+ this.playTone(toneDef.d, toneDef.f1, toneDef.f2, function () {
2102
+ setTimeout(self._doRepeatTone.bind(self), toneDef.g);
2103
+ });
2104
+ }
2105
+ }
2106
+
2107
+ // End repeating tones
2108
+ _endRepeatTone() {
2109
+ if (this._repeatToneActive) {
2110
+ this.log.info("Repeating tone stopped");
2111
+ this._repeatToneActive = false;
2112
+ this.stopTone();
2113
+ }
2114
+ }
2115
+
2116
+ // Initialize tone generator
2117
+ _initToneGenerator() {
2118
+ // Create audio context
2119
+ const AudioContext = window.AudioContext || window.webkitAudioContext || false;
2120
+ if (!AudioContext) {
2121
+ throw new NotAllowed("Audio API not supported by this browser");
2122
+ }
2123
+
2124
+ const ctx = new AudioContext();
2125
+ this._toneStreamNode = ctx.createMediaStreamDestination();
2126
+ this._toneAudioElement.srcObject = this._toneStreamNode.stream;
2127
+
2128
+ this._toneGainProcessor = ctx.createGain();
2129
+ this._toneGainProcessor.gain.value = 0;
2130
+ this._toneGainProcessor.connect(this._toneStreamNode);
2131
+
2132
+ // Create two oscillators and start them
2133
+ this._oscillator1 = ctx.createOscillator();
2134
+ this._oscillator1.connect(this._toneGainProcessor);
2135
+ this._oscillator1.frequency.value = 0;
2136
+ this._oscillator1.start();
2137
+
2138
+ this._oscillator2 = ctx.createOscillator();
2139
+ this._oscillator2.connect(this._toneGainProcessor);
2140
+ this._oscillator2.frequency.value = 0;
2141
+ this._oscillator2.start();
2142
+ }
2143
+
2144
+ /**
2145
+ * Hold all active calls
2146
+ * @throws NotAllowed If there are connection calls
2147
+ */
2148
+ holdAllCalls() {
2149
+ this.log.info("Holding all active sessions");
2150
+ // Check all session can be set on hold
2151
+ if (this._checkForConnectingCalls()) {
2152
+ throw NotAllowed("A session is currently connecting");
2153
+ }
2154
+
2155
+ // Iterate through all active calls and hold them
2156
+ for (let key in this._activeSessions) {
2157
+ if (this._activeSessions.hasOwnProperty(key)) {
2158
+ let session = this._activeSessions[key];
2159
+ if (session.state === "ACTIVE") {
2160
+ session.hold();
2161
+ }
2162
+ }
2163
+ }
2164
+ }
2165
+
2166
+ /*
2167
+ * ======= PRIVATE METHODS PHONE CLASS =========================================================================
2168
+ */
2169
+
2170
+ // Add a new session
2171
+ _addSession(session) {
2172
+ /**
2173
+ * @event sessionCreated
2174
+ * @type {object}
2175
+ * @param {session} session - New session created
2176
+ */
2177
+ // Give session unique id
2178
+ this._sessionCounter++;
2179
+ session.__containerId = this._sessionCounter;
2180
+
2181
+ this.log.info(`Adding new session ${session.__containerId}: ${session.uuid}`);
2182
+ this._activeSessions[session.__containerId] = session;
2183
+ this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions:`);
2184
+ this.log.debug(this._activeSessions);
2185
+
2186
+ // Fire event
2187
+ try {
2188
+ this.emit("sessionCreated", { session: session });
2189
+ } catch (e) {
2190
+ this.log.error("Exception in sessionCreated event: " + e);
2191
+ }
2192
+ }
2193
+
2194
+ // Remove session
2195
+ _removeSession(session) {
2196
+ /**
2197
+ * @event sessionRemoved
2198
+ * @type {object}
2199
+ * @param {session} session - Session removed
2200
+ */
2201
+
2202
+ this.log.info(`Removing session ${session.__containerId}: ${session.uuid}`);
2203
+
2204
+ // Fire event
2205
+ try {
2206
+ this.emit("sessionRemoved", { session: session });
2207
+ } catch (e) {
2208
+ this.log.error("Exception in sessionRemoved event: " + e);
2209
+ }
2210
+
2211
+ delete this._activeSessions[session.__containerId];
2212
+ this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions`);
2213
+ this.log.debug(this._activeSessions);
2214
+
2215
+ // Stop any repeating tones
2216
+ this._endRepeatTone();
2217
+ }
2218
+
2219
+ // Session call this when call is finished
2220
+ _sessionTerminated(session) {
2221
+ this.log.debug(`Session ${session.uuid} terminated, removing in ${this._options.autoRemoveSessionTime} ms`);
2222
+ const self = this;
2223
+ setTimeout(function () {
2224
+ self._removeSession(session);
2225
+ }, this._options.autoRemoveSessionTime);
2226
+ }
2227
+
2228
+ // Return true if there are calls currently connecting
2229
+ _checkForConnectingCalls() {
2230
+ for (let key in this._activeSessions) {
2231
+ if (this._activeSessions.hasOwnProperty(key)) {
2232
+ let session = this._activeSessions[key];
2233
+ if (session.state === "INITIALIZING" || session.state === "CALLING" || session.state === "PROGRESS" || session.state === "INCOMING") {
2234
+ return true;
2235
+ }
2236
+ }
2237
+ }
2238
+ return false;
2239
+ }
2240
+
2241
+ /**
2242
+ * @typedef {string} CONNECTION_STATE
2243
+ * @property {string} CONNECTING Connecting to server
2244
+ * @property {string} CONNECTED Connected to server
2245
+ * @property {string} DISCONNECTED Disconnected from server
2246
+ * @property {string} REGISTERED Registered on server
2247
+ * @property {string} UNREGISTERED De-registered from server
2248
+ * @property {string} REGISTRATION_FAILED Registration failed
2249
+ */
2250
+ // Set connection state and emit event
2251
+ _setConnectionState(state) {
2252
+ /**
2253
+ * @event connectionStateChange
2254
+ * @type {object}
2255
+ * @param {CONNECTION_STATE} state - The current connection state
2256
+ */
2257
+
2258
+ this.log.info("Connection state: " + state);
2259
+ this._connectionState = state;
2260
+ try {
2261
+ this.emit("connectionStateChange", { state: state });
2262
+ } catch (e) {
2263
+ this.log.error("Exception in connectionStateChange event: " + e);
2264
+ }
2265
+ }
2266
+
2267
+ /**
2268
+ * @event incomingCall
2269
+ * @type {object}
2270
+ * @param {Session} session New incoming session
2271
+ */
2272
+ _fireIncomingCallEvent(session) {
2273
+ try {
2274
+ this.emit("incomingCall", session);
2275
+ } catch (e) {
2276
+ this.log.error("Exception in incomingCall event: " + e);
2277
+ }
2278
+ }
2279
+
2280
+ // Fire call data event to subscribers
2281
+ _fireCallDataInfoEvent(eventData = null) {
2282
+ /**
2283
+ * @event callDataInfo
2284
+ *
2285
+ * @type {object}
2286
+ * @property {uuid} uuid - Customer Call UUID for session
2287
+ * @param {CALL_DIRECTION} direction - Call direction
2288
+ * @param {CALL_DISPOSITION} disposition - The final call disposition
2289
+ * @param {string} originatingNumber - Caller ID number (a-number) used for call
2290
+ * @param {string} destinationNumber - Destination number (b-number) used for call
2291
+ * @param {Date} startTime - Start time of call
2292
+ * @param {Date} answerTime - Time call was answered
2293
+ * @param {Date} endTime - End time of call
2294
+ * @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
2295
+ * @param {String} durationStr - Total duration of the call formatted as (mm:ss)
2296
+ * @param {number} answerDuration - Time before call was answered (millisec)
2297
+ * @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
2298
+ * @param {number} activeDuration - Active duration of the call (time spend talking) (millisec)
2299
+ * @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
2300
+ * @param {number} rateDestinationId - Rate destination id
2301
+ * @param {string} rateDestinationStr - Rate destination name
2302
+ * @param {number} rateDestinationCost - Rate destination cost / minute
2303
+ * @param {number} callConnectFee - Call attempt/connect cost
2304
+ * @param {number} effectiveCidId - Phone number id used as a-number for call
2305
+ * @param {number} effectiveCidNumberStr - Phone number used as a-number for call
2306
+ * @param {string} effectiveCidNameStr - Phone number name used as a-number for call
2307
+ * @param {boolean} callPrivacy - Call privacy used for call (number hidden)
2308
+ * @param {number} hangupCauseQ850 - Q.850 hangup cause code
2309
+ * @param {Object[]} recordings - List of recordings done for this call
2310
+ * @param {number} recordings[].startTime - Time recording was started
2311
+ * @param {number} recordings[].length - Recording length in milliseconds
2312
+ * @param {number} recordings[].size - Recording size (bytes)
2313
+ * @param {number} recordings[].urlPlayer - URL for recording with player
2314
+ * @param {number} recordings[].url - URL for recording without player
2315
+ * @param {number} recordings[].retentionDate - Date recording will be automatically deleted
2316
+ */
2317
+
2318
+ let e;
2319
+ e = camelcaseKeys(eventData);
2320
+ e.recordings = camelcaseKeys(eventData.recordings);
2321
+
2322
+ // Event all duration fields are in seconds, convert to millisecs
2323
+ e.duration = e.duration * 1000;
2324
+ e.answerDuration = e.answerDuration * 1000;
2325
+ e.activeDuration = e.activeDuration * 1000;
2326
+
2327
+ // Add some extra convenience fields
2328
+ e.durationStr = createDurationString(e.duration);
2329
+ e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) : "";
2330
+ e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) : "";
2331
+ try {
2332
+ this.emit("callDataInfo", e);
2333
+ } catch (e) {
2334
+ this.log.error("Exception in callDataInfo event: " + e);
2335
+ }
2336
+ }
2337
+
2338
+ // Check if we have media permission
2339
+ _checkMediaPermission() {
2340
+ // Check media is prepared
2341
+ if (!this._mediaPermission) {
2342
+ throw new MediaNotPrepared();
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ /** Session class created for every active call. You should use either call() or event incomingCall() to get new sessions
2348
+ *
2349
+ * @fires callEnded Receive basic information about call after call has been terminated
2350
+ * @fires callDurationUpdate Regular updates on call duration (every second)
2351
+ * @fires sessionStateChange Changes to session state
2352
+ * @fires recordingStateChange When recording state changes
2353
+ * @fires muteStateChange When mute state changes
2354
+ * @fires holdStateChange When call is hold/unload
2355
+ *
2356
+ */
2357
+ class Session extends EventEmitter {
2358
+ constructor(phone) {
2359
+ super();
2360
+
2361
+ // Parent Phone object
2362
+ this._phone = phone;
2363
+
2364
+ // Use log instance from phone object
2365
+ this.log = phone.log;
2366
+
2367
+ this._callUUID = null;
2368
+ this._webRTCSession = null;
2369
+ this._sessionState = null;
2370
+ this._iceCandidateTimer = null;
2371
+ this._isOnHold = false;
2372
+
2373
+ // Audio device flags
2374
+ this._isMuted = false;
2375
+ this._isRecording = false;
2376
+ this._recordingError = null;
2377
+
2378
+ // Call timers
2379
+ this._callBeginTime = null;
2380
+ this._callAnswerTime = null;
2381
+ this._callEndTime = null;
2382
+ this._durationUpdTimerObj = null;
2383
+
2384
+ // Call information
2385
+ this._originatingNumber = null;
2386
+ this._destinationNumber = null;
2387
+ this._callDisposition = null;
2388
+ this._localNumberName = null;
2389
+ this._isListenCall = false;
2390
+ this._whisperMode = null;
2391
+ this._manualDisposition = null;
2392
+
2393
+ // Quality report
2394
+ this._rtcpStatsTimer = null;
2395
+ this._currentReport = null;
2396
+
2397
+ // Timeout
2398
+ this._answerCallTimeout = 0;
2399
+ this._answerCallTimer = null;
2400
+
2401
+ // Current media switch handling the call
2402
+ this._mediaSwitch = null;
2403
+ }
2404
+
2405
+ // Create a new outbound session
2406
+ newOutboundSession(number, callParams) {
2407
+ // First create a UUID
2408
+ this._callUUID = createUUID();
2409
+ this.log.debug(`Created new outbound session to: ${number} [${this._callUUID}]`);
2410
+ this.destinationNumber = number;
2411
+
2412
+ let options = {
2413
+ mediaConstraints: { audio: true, video: false },
2414
+ pcConfig: {
2415
+ iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
2416
+ },
2417
+ extraHeaders: ["X-Call-token: " + this._phone.options.callToken, "X-P-UUID: " + this._callUUID, "X-Max-cost: " + callParams.maximumCallCost],
2418
+ };
2419
+
2420
+ // Append explicit caller id if needed
2421
+ if (callParams.explicitCallerId) {
2422
+ options.extraHeaders.push("X-CID-Number-Id: " + callParams.explicitCallerId);
2423
+ }
2424
+
2425
+ // Append explicit short cid if needed
2426
+ if (callParams.explicitShortCallerId) {
2427
+ options.extraHeaders.push("X-Short-CID-Id: " + callParams.explicitShortCallerId);
2428
+ }
2429
+
2430
+ // Append meta info
2431
+ if (callParams.metaInfo) {
2432
+ options.extraHeaders.push("X-Meta-Info: " + encodeURI(callParams.metaInfo.toString().substr(0, 255)));
2433
+ }
2434
+
2435
+ // Enable recording?
2436
+ if (callParams.record) {
2437
+ if (isNaN(callParams.record)) {
2438
+ throw new InvalidParameter("Record parameter must be a number");
2439
+ }
2440
+ this.log.debug(`Recording enabled and kept for ${callParams.record} days`);
2441
+ options.extraHeaders.push("X-Record: " + callParams.record);
2442
+ this._isRecording = true;
2443
+ } else {
2444
+ this._isRecording = false;
2445
+ }
2446
+
2447
+ // Store listen flag
2448
+ this._isListenCall = callParams._isListenCall;
2449
+
2450
+ // Manually specified media switch
2451
+ if (callParams.mediaSwitchURI) {
2452
+ this.log.debug("Requesting specific media switch " + callParams.mediaSwitchURI);
2453
+ options.extraHeaders.push("X-RMS: " + callParams.mediaSwitchURI);
2454
+ } else {
2455
+ // Request specific media switch to group calls on same server
2456
+ const otherSessions = this.getOtherSessions();
2457
+
2458
+ if (otherSessions.length > 0) {
2459
+ if (otherSessions[0]._mediaSwitch) {
2460
+ // There are existing active calls, append first media switch address found and request call grouping to this
2461
+ this.log.debug("Requesting call grouping on media switch " + otherSessions[0]._mediaSwitch);
2462
+ options.extraHeaders.push("X-RMS: sip:" + otherSessions[0]._mediaSwitch);
2463
+ } else {
2464
+ this.log.warning("Other sessions exist but media switch address is invalid");
2465
+ }
2466
+ } else {
2467
+ this.log.debug("No other active calls, no media switch grouping requested");
2468
+ }
2469
+ }
2470
+
2471
+ // Prepare invite uri
2472
+ const uri = "sip:" + number + "@telegenta.com";
2473
+
2474
+ // Not start call and create sip session
2475
+ try {
2476
+ this._initWebRTCSession(this._phone._UA.call(uri, options));
2477
+ } catch (error) {
2478
+ this.log.error("Error starting call:", error);
2479
+ return null;
2480
+ }
2481
+
2482
+ if (callParams.answerCallTimeout) {
2483
+ this._setAnswerCallTimeout(callParams.answerCallTimeout);
2484
+ }
2485
+ }
2486
+
2487
+ // Create new inbound session
2488
+ newInboundSession(newSessionRequest) {
2489
+ this._callUUID = newSessionRequest.request.getHeader("X-P-UUID", 0);
2490
+ this._destinationNumber = newSessionRequest.request.getHeader("X-DID", 0);
2491
+ this._localNumberName = unescape(newSessionRequest.request.getHeader("X-DID-NAME", 0));
2492
+ this._originName = newSessionRequest.request.from.display_name;
2493
+ this._originatingNumber = newSessionRequest.request.from.uri.user;
2494
+ this.log.info(`Incoming call from ${this._originatingNumber} [${this._callUUID}]`);
2495
+ this.log.debug(newSessionRequest);
2496
+
2497
+ // Get media switch info
2498
+ this._mediaSwitch = newSessionRequest.request.getHeader("X-MS");
2499
+ this.log.debug(`Media switch handling call: ${this._mediaSwitch}`);
2500
+
2501
+ this._initWebRTCSession(newSessionRequest.session);
2502
+ }
2503
+
2504
+ /**
2505
+ * Return a list of all sessions except this session
2506
+ * Mostly used when transferring call
2507
+ */
2508
+ getOtherSessions() {
2509
+ const allSessions = this._phone.sessions;
2510
+
2511
+ let s = [];
2512
+ for (let key in allSessions) {
2513
+ if (allSessions[key]) {
2514
+ if (allSessions[key].__containerId !== this.__containerId) {
2515
+ s.push(allSessions[key]);
2516
+ }
2517
+ }
2518
+ }
2519
+
2520
+ return s;
2521
+ }
2522
+
2523
+ // Initialize with WebRTCSession object
2524
+ _initWebRTCSession(webRTCSession) {
2525
+ this._webRTCSession = webRTCSession;
2526
+ this._callBeginTime = Date.now();
2527
+
2528
+ // Hookup events
2529
+ const self = this;
2530
+
2531
+ // ICE CANDIDATES
2532
+ this._webRTCSession.on("icecandidate", function (event) {
2533
+ const c = event.candidate;
2534
+ self.log.debug(`Got ICE candidate: ${c.candidate}`);
2535
+ if (c.type === "srflx" && c.relatedAddress !== null && c.relatedPort !== null) {
2536
+ self.log.info(`Accepting ICE candidate: ${c.candidate}`);
2537
+
2538
+ // Clear timer first
2539
+ if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
2540
+
2541
+ // Now fire ready
2542
+ event.ready();
2543
+ } else {
2544
+ // Start timeout
2545
+ if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
2546
+
2547
+ self._iceCandidateTimer = setTimeout(
2548
+ function (data) {
2549
+ self.log.debug(`ICE Timeout (${self._phone.options.iceGatherTimeout} ms) reached`);
2550
+ data.ready();
2551
+ },
2552
+ self._phone.options.iceGatherTimeout,
2553
+ event,
2554
+ );
2555
+ }
2556
+ });
2557
+
2558
+ this._webRTCSession.on("connecting", function (/* data */) {
2559
+ self._startDurationUpdateTimer();
2560
+ self._startRTCPStatsUpdateTimer();
2561
+ self._setSessionState("INITIALIZING");
2562
+ });
2563
+
2564
+ this._webRTCSession.on("sending", function (/* data */) {
2565
+ // Play call initialized tone if needed
2566
+ if (self._phone.options.playCallInitializedTone) {
2567
+ self._phone._startRepeatTone(ST_CALLINIT);
2568
+ }
2569
+
2570
+ self._setSessionState("CALLING");
2571
+ });
2572
+
2573
+ this._webRTCSession.on("progress", function (data) {
2574
+ if (data.originator === "remote") {
2575
+ self._setSessionState("PROGRESS", { code: data.response.status_code });
2576
+ self._phone._endRepeatTone();
2577
+
2578
+ // Ringback handling on 180 RINGING
2579
+ if (data.response.status_code === 180) {
2580
+ self._phone._startRepeatTone(ST_RINGBACK);
2581
+ }
2582
+
2583
+ // Also start answer call timeout
2584
+ if (self._answerCallTimeout) {
2585
+ self._answerCallTimer = setTimeout(function () {
2586
+ self.log.info(`Answer call timeout after ${self._answerCallTimeout} seconds`);
2587
+ try {
2588
+ self.hangup();
2589
+ } catch (e) {
2590
+ self.log.info("No active session, ignoring answer call timeout");
2591
+ }
2592
+ }, self._answerCallTimeout * 1000);
2593
+ }
2594
+ }
2595
+ });
2596
+
2597
+ this._webRTCSession.on("accepted", function (data) {
2598
+ // Always end ringsback
2599
+ self._phone._endRepeatTone();
2600
+
2601
+ self._callAnswerTime = Date.now();
2602
+
2603
+ // Call was answered, clear answer call timeout timer if needed
2604
+ if (self._answerCallTimeout) {
2605
+ clearTimeout(self._answerCallTimer);
2606
+ self._answerCallTimeout = 0;
2607
+ }
2608
+
2609
+ // Get initial recording state
2610
+ if (data.originator === "remote") {
2611
+ const recState = data.response.getHeader("X-Recording");
2612
+ if (recState === "true") {
2613
+ self.log.debug("Call is being prerecorded");
2614
+ // self._isRecording = true;
2615
+ // self.emit('recordingStateChange', {state: true, error: null});
2616
+ } else {
2617
+ self.log.debug("No call recording started yet");
2618
+ }
2619
+
2620
+ // Get media switch info
2621
+ self._mediaSwitch = data.response.getHeader("X-MS");
2622
+ self.log.debug(`Media switch handling call: ${self._mediaSwitch}`);
2623
+ }
2624
+ });
2625
+
2626
+ this._webRTCSession.on("confirmed", function (/* data */) {
2627
+ self._setSessionState("ACTIVE");
2628
+ });
2629
+
2630
+ this._webRTCSession.on("ended", function (/* data */) {
2631
+ // End any ringsbacks if needed
2632
+ self._phone._endRepeatTone();
2633
+
2634
+ self._callEndTime = Date.now();
2635
+
2636
+ // Clear answer call timeout timer if needed
2637
+ if (self._answerCallTimeout) {
2638
+ clearTimeout(self._answerCallTimer);
2639
+ }
2640
+
2641
+ self._setSessionState("TERMINATED");
2642
+ });
2643
+
2644
+ this._webRTCSession.on("failed", function (data) {
2645
+ // End any ringsbacks if needed
2646
+ self._phone._endRepeatTone();
2647
+
2648
+ // self.log.error(data);
2649
+ self._callEndTime = Date.now();
2650
+
2651
+ // Clear answer call timeout timer if needed
2652
+ if (self._answerCallTimeout) {
2653
+ clearTimeout(self._answerCallTimer);
2654
+ }
2655
+
2656
+ let m;
2657
+
2658
+ // Append message if available
2659
+ if (data.message) {
2660
+ m = `${data.message.status_code} ${data.message.reason_phrase}`;
2661
+ } else {
2662
+ m = "";
2663
+ }
2664
+
2665
+ self._setSessionState("FAILED", { cause: data.cause, message: m });
2666
+ });
2667
+
2668
+ this._webRTCSession.on("dtmf", function (data) {
2669
+ self.log.debug("Session DTMF", data);
2670
+ });
2671
+
2672
+ this._webRTCSession.on("newInfo", function (data) {
2673
+ self.log.debug("New session info", data);
2674
+ });
2675
+
2676
+ this._webRTCSession.on("hold", function (/* data */) {
2677
+ self._isOnHold = true;
2678
+ self._fireHoldStateEvent();
2679
+ self._setSessionState("ON_HOLD");
2680
+ });
2681
+
2682
+ this._webRTCSession.on("unhold", function (/* data */) {
2683
+ self._isOnHold = false;
2684
+ self._fireHoldStateEvent();
2685
+ self._setSessionState("ACTIVE");
2686
+ });
2687
+
2688
+ this._webRTCSession.on("muted", function (data) {
2689
+ self.log.debug("Microphone muted", data);
2690
+ self._isMuted = true;
2691
+ self._fireMuteStateEvent();
2692
+ });
2693
+
2694
+ this._webRTCSession.on("unmuted", function (data) {
2695
+ self.log.debug("Microphone unmuted", data);
2696
+ self._isMuted = false;
2697
+ self._fireMuteStateEvent();
2698
+ });
2699
+
2700
+ this._webRTCSession.on("reinvite", function (data) {
2701
+ self.log.debug("Session reinvite", data);
2702
+ });
2703
+
2704
+ this._webRTCSession.on("update", function (data) {
2705
+ self.log.debug("Session update", data);
2706
+ });
2707
+
2708
+ this._webRTCSession.on("getusermediafailed", function (data) {
2709
+ self.log.error("Getusermedia failed", data);
2710
+ });
2711
+
2712
+ // If connection connect audio stream
2713
+ if (self._webRTCSession.connection) {
2714
+ self._handleRemoteAudio(self._webRTCSession);
2715
+ }
2716
+
2717
+ // DEBUG STUFF
2718
+ this.log.debug("WebRTCSession object:");
2719
+ this.log.debug(this);
2720
+ }
2721
+
2722
+ /**
2723
+ * Hangup call
2724
+ * @param {CALL_DISPOSITION} disposition - Hangup call with specific disposition
2725
+ * @throws NoActiveSession - If no active session / call exists
2726
+ */
2727
+ hangup(disposition = null) {
2728
+ if (!disposition) {
2729
+ this.log.info("Hanging up call");
2730
+ this._webRTCSession.terminate();
2731
+ } else {
2732
+ this.log.info(`Hanging up call with disposition ${disposition}`);
2733
+ this._manualDisposition = disposition;
2734
+ this._webRTCSession.terminate({
2735
+ extraHeaders: ["X-HDISP: " + disposition],
2736
+ });
2737
+ }
2738
+ }
2739
+
2740
+ /**
2741
+ * Answer incoming call
2742
+ * @param metaInfo - Add meta info to this call
2743
+ */
2744
+ answer(metaInfo = null) {
2745
+ let options = {
2746
+ mediaConstraints: { audio: true, video: false },
2747
+ pcConfig: {
2748
+ iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
2749
+ },
2750
+ extraHeaders: ["X-Call-token: " + this._phone.options.callToken],
2751
+ };
2752
+
2753
+ // Append meta info if needed
2754
+ if (metaInfo) {
2755
+ options.extraHeaders.push("X-Meta-Info: " + encodeURI(metaInfo.toString().substr(0, 255)));
2756
+ }
2757
+
2758
+ this._webRTCSession.answer(options);
2759
+
2760
+ // After answer we start remote audio
2761
+ this._handleRemoteAudio(this._webRTCSession);
2762
+ }
2763
+
2764
+ /**
2765
+ * Reject incoming call
2766
+ * @param statusCode - SIP Status code
2767
+ * @param reasonPhrase - Reason phrase
2768
+ */
2769
+ reject(statusCode = 603, reasonPhrase = "Rejected call") {
2770
+ this.log.info("Rejecting call");
2771
+ this._webRTCSession.terminate({
2772
+ status_code: statusCode,
2773
+ reason_phrase: reasonPhrase,
2774
+ });
2775
+ }
2776
+
2777
+ /**
2778
+ * Return UUID of call
2779
+ * @returns {string} UUID of call
2780
+ */
2781
+ get uuid() {
2782
+ return this._callUUID;
2783
+ }
2784
+
2785
+ /**
2786
+ * Destination number called
2787
+ * @returns {string} called number
2788
+ */
2789
+ get destinationNumber() {
2790
+ return this._destinationNumber;
2791
+ }
2792
+
2793
+ /**
2794
+ * Name of local number (for inbound)
2795
+ * @returns {string} Name of local number called
2796
+ */
2797
+ get localNumberName() {
2798
+ return this._localNumberName;
2799
+ }
2800
+
2801
+ /**
2802
+ * Originating number
2803
+ * @returns {string} Number of calling party
2804
+ */
2805
+ get originatingNumber() {
2806
+ return this._originatingNumber;
2807
+ }
2808
+
2809
+ // Set destination number
2810
+ set destinationNumber(number) {
2811
+ this.log.debug("Setting destination number to: " + number);
2812
+ this._destinationNumber = number;
2813
+ }
2814
+
2815
+ /**
2816
+ * Session state
2817
+ * @returns {SESSION_STATE} current session state
2818
+ */
2819
+ get state() {
2820
+ return this._sessionState;
2821
+ }
2822
+
2823
+ /**
2824
+ * Session call direction
2825
+ * @returns {CALL_DIRECTION} current session state
2826
+ */
2827
+ get direction() {
2828
+ if (this._isListenCall) {
2829
+ this.log.debug("Call direction is LISTEN");
2830
+ return "LISTEN";
2831
+ } else if (this._webRTCSession.direction === "incoming") {
2832
+ this.log.debug("Call direction is INBOUND");
2833
+ return "INBOUND";
2834
+ } else if (this._webRTCSession.direction === "outgoing") {
2835
+ this.log.debug("Call direction is OUTBOUND");
2836
+ return "OUTBOUND";
2837
+ } else {
2838
+ this.log.error("Unknown call direction: " + this._webRTCSession.direction);
2839
+ return "UNKNOWN";
2840
+ }
2841
+ }
2842
+
2843
+ // RECORDING
2844
+ /**
2845
+ * Start recording
2846
+ */
2847
+ startRecording() {
2848
+ const opt = {
2849
+ extraHeaders: ["Record: on"],
2850
+ };
2851
+
2852
+ this.log.debug("Request start recording");
2853
+
2854
+ const self = this;
2855
+ this._webRTCSession.once("newInfo", (data) => {
2856
+ if (data.originator === "local") {
2857
+ data.info.on("succeeded", function (/* data */) {
2858
+ self.log.info("Recording started successfully");
2859
+ self._isRecording = true;
2860
+ try {
2861
+ self.emit("recordingStateChange", { state: true, error: null });
2862
+ } catch (e) {
2863
+ self.log.error("Exception in recordingStateChange event: " + e);
2864
+ }
2865
+ });
2866
+
2867
+ data.info.on("failed", function (data) {
2868
+ self._recordingError = data.response.reason_phrase;
2869
+ self.log.error("Recording start failed: " + self._recordingError);
2870
+ self._fireRecordingStateEvent();
2871
+ });
2872
+ }
2873
+ });
2874
+
2875
+ this._webRTCSession.sendInfo("application/info", null, opt);
2876
+ }
2877
+
2878
+ /**
2879
+ * Stop recording
2880
+ */
2881
+ stopRecording() {
2882
+ const opt = {
2883
+ extraHeaders: ["Record: off"],
2884
+ };
2885
+
2886
+ this.log.debug("Request stop recording");
2887
+
2888
+ const self = this;
2889
+
2890
+ this._webRTCSession.once("newInfo", (data) => {
2891
+ data.info.on("succeeded", function (/* data */) {
2892
+ self.log.info("Recording stopped successfully");
2893
+ self._isRecording = false;
2894
+ try {
2895
+ self.emit("recordingStateChange", { state: false, error: null });
2896
+ } catch (e) {
2897
+ self.log.error("Exception in recordingStateChange event: " + e);
2898
+ }
2899
+ });
2900
+
2901
+ data.info.on("failed", function (data) {
2902
+ self.log.error("Recording stopped failed: " + data.response.reason_phrase);
2903
+ try {
2904
+ self.emit("recordingStateChange", { state: self._isRecording, error: data.response.reason_phrase });
2905
+ } catch (e) {
2906
+ self.log.error("Exception in recordingStateChange event: " + e);
2907
+ }
2908
+ });
2909
+ });
2910
+
2911
+ this._webRTCSession.sendInfo("application/info", null, opt);
2912
+ }
2913
+
2914
+ /**
2915
+ * Toogle recording ON/OFF
2916
+ */
2917
+ toggleRecording() {
2918
+ if (!this._isRecording) {
2919
+ this.startRecording();
2920
+ return true;
2921
+ } else {
2922
+ this.stopRecording();
2923
+ return false;
2924
+ }
2925
+ }
2926
+
2927
+ /**
2928
+ * Return recording state
2929
+ * @return {boolean}
2930
+ */
2931
+ get isRecording() {
2932
+ return this._isRecording;
2933
+ }
2934
+
2935
+ // MUTE
2936
+ /**
2937
+ * Mute microphone
2938
+ */
2939
+ muteMic() {
2940
+ this.log.info("Mute microphone");
2941
+ this._webRTCSession.mute();
2942
+ }
2943
+
2944
+ /**
2945
+ * Unmute microphone
2946
+ */
2947
+ unmuteMic() {
2948
+ this.log.info("Unmute microphone");
2949
+ this._webRTCSession.unmute();
2950
+ }
2951
+
2952
+ /**
2953
+ * Toggle microphone mute state
2954
+ */
2955
+ toggleMute() {
2956
+ if (!this._isMuted) {
2957
+ this.muteMic();
2958
+ } else {
2959
+ this.unmuteMic();
2960
+ }
2961
+ }
2962
+
2963
+ /**
2964
+ * Return mute state
2965
+ * @return {bool}
2966
+ */
2967
+ get isMuted() {
2968
+ return this._isMuted;
2969
+ }
2970
+
2971
+ // HOLD
2972
+ /**
2973
+ * Hold call
2974
+ */
2975
+ hold() {
2976
+ this.log.info("Hold call");
2977
+ this._webRTCSession.hold();
2978
+ }
2979
+
2980
+ /**
2981
+ * Unhold call
2982
+ */
2983
+ unhold() {
2984
+ this.log.info("Uncall call");
2985
+ this._phone.holdAllCalls();
2986
+ this._webRTCSession.unhold();
2987
+ }
2988
+
2989
+ /**
2990
+ * Toggle call hold
2991
+ */
2992
+ toggleHold() {
2993
+ if (!this._isOnHold) {
2994
+ this.hold();
2995
+ } else {
2996
+ this.unhold();
2997
+ }
2998
+ }
2999
+
3000
+ /**
3001
+ * Return call hold status
3002
+ * @return {bool}
3003
+ */
3004
+ get isOnHold() {
3005
+ return this._isOnHold;
3006
+ }
3007
+
3008
+ /**
3009
+ * Send DTMF tone on call
3010
+ * @param code What DTMF code to send (valid: 0-9,*,#,d)
3011
+ * @param playLocal=True Play DTMF sound locally also
3012
+ * @throws InvalidParameter - On incorrect DTMF codes
3013
+ */
3014
+ sendDTMF(code, playLocal = true) {
3015
+ // Check for correct DTMF and get frequencies
3016
+ if (!(code in DTMF_FREQUENCY_TABLE)) {
3017
+ throw new InvalidParameter("Invalid DTMF code");
3018
+ }
3019
+
3020
+ this.log.info("Sending DTMF: " + code);
3021
+ this._webRTCSession.sendDTMF(code);
3022
+
3023
+ if (playLocal) {
3024
+ const f = DTMF_FREQUENCY_TABLE[code];
3025
+
3026
+ // First mute microphone before playing DTMF
3027
+ if (this._isMuted) {
3028
+ this.log.debug("Mic already muted, just sending DTMF");
3029
+ this._phone.playTone(500, f[0], f[1]);
3030
+ } else {
3031
+ this.log.debug("Mute mic while sending DTMF");
3032
+ this._webRTCSession.mute();
3033
+
3034
+ const self = this;
3035
+ this._phone.playTone(500, f[0], f[1], function () {
3036
+ self.log.debug("Unmute mic after DTMF");
3037
+ self._webRTCSession.unmute();
3038
+ });
3039
+ }
3040
+ }
3041
+ }
3042
+
3043
+ /**
3044
+ * Transfer call to another session (attended transfer)
3045
+ *
3046
+ * @param dstSession Destination session
3047
+ */
3048
+ transfer(dstSession) {
3049
+ this.log.info(`Transferring ${this.uuid} => ${dstSession.uuid}`);
3050
+
3051
+ // Create ReferSubscriber event class
3052
+ const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, { replaces: dstSession._webRTCSession });
3053
+
3054
+ const self = this;
3055
+
3056
+ // Setup some logging
3057
+ referSubscriber.on("requestSucceeded", function () {
3058
+ self.log.info("Transfer completed successfully");
3059
+ });
3060
+
3061
+ referSubscriber.on("requestFailed", function (cause) {
3062
+ self.log.error(`Transfer rejected: ${cause}`);
3063
+ });
3064
+
3065
+ referSubscriber.on("failed", function (cause) {
3066
+ self.log.error(`Transfer failed: ${cause}`);
3067
+ });
3068
+ }
3069
+
3070
+ /**
3071
+ * Playback media file into call. You must be connected the event service to use this feature (set connectEventService=true)
3072
+ *
3073
+ * @param media_file_id Media file to playback
3074
+ * @param hangup_after Hangup call after playback ends
3075
+ *
3076
+ * @throws NotAllowed - If event service is not enabled
3077
+ *
3078
+ * @return Promise that resolves when playback has started
3079
+ */
3080
+ playback(media_file_id, hangup_after = false) {
3081
+ // Check event service is enabled first
3082
+ if (!this._phone._options.connectEventService) {
3083
+ throw new NotAllowed("Event service not enabled");
3084
+ }
3085
+
3086
+ this.log.info(`Playback media file ${media_file_id} hangup after ${hangup_after}`);
3087
+
3088
+ const p = this._phone._eventClient.fireEvent("call_command", {
3089
+ call_uuid: this.uuid,
3090
+ call_command: "playback",
3091
+ media_file_id: media_file_id,
3092
+ hangup_after: hangup_after,
3093
+ });
3094
+
3095
+ // When playback is acknowledged and 'hangup_after' is true, we set session state into PLAYBACK
3096
+ if (hangup_after) {
3097
+ // Stop remote audio
3098
+ this._phone._remoteAudioElement.srcObject = null;
3099
+
3100
+ // Mute microphone
3101
+ this._webRTCSession.mute();
3102
+
3103
+ // When playback starts set state to playback
3104
+ p.then(() => {
3105
+ // Set session state
3106
+ this._setSessionState("PLAYBACK");
3107
+ });
3108
+ }
3109
+
3110
+ return p;
3111
+ }
3112
+
3113
+ /**
3114
+ * Set whisper mode on "listen" calls
3115
+ *
3116
+ * @param mode Whisper mode 0 = No whisper / 1 = local peer only / 2 = Three way (both local and remote)
3117
+ *
3118
+ * @throws NotAllowed - If call is not listen call
3119
+ * @throws InvalidOptions - If invalid mode given
3120
+ */
3121
+ setWhisperMode(mode = 0) {
3122
+ if (!this.direction === "LISTEN") {
3123
+ throw new NotAllowed("Call is not correct type (listen)");
3124
+ }
3125
+
3126
+ if (mode < 0 || mode > 2) {
3127
+ throw new InvalidOptions("Invalid mode");
3128
+ }
3129
+
3130
+ let t_dtmf_mode;
3131
+
3132
+ if (mode === 0) {
3133
+ t_dtmf_mode = 0;
3134
+ }
3135
+
3136
+ if (mode === 1) {
3137
+ t_dtmf_mode = 2;
3138
+ }
3139
+
3140
+ if (mode === 2) {
3141
+ t_dtmf_mode = 3;
3142
+ }
3143
+
3144
+ this.sendDTMF(t_dtmf_mode, false);
3145
+ this._whisperMode = mode;
3146
+ this._fireWhisperModeChangeEvent();
3147
+ }
3148
+
3149
+ /*
3150
+ * ======= PRIVATE METHODS PHONE SESSION =========================================================================
3151
+ */
3152
+
3153
+ // Begin update timer events
3154
+ _startDurationUpdateTimer() {
3155
+ /**
3156
+ * @event callDurationUpdate
3157
+ * @type {object}
3158
+ * @property {String} uuid - UUID of session
3159
+ * @property {Date} beginTime - Time session was created
3160
+ * @property {Date} answerTime - Time session was answered
3161
+ * @property {Date} duration - The total duration of the session
3162
+ * @property {String} durationStr - The duration of the session string formated (mm:ss)
3163
+ * @property {Date} activeDuration - The active duration of the session
3164
+ * @property {String} activeDurationStr - The active duration of the session string formated (mm:ss)
3165
+ *
3166
+ * @example
3167
+ * // Update duration text on screen
3168
+ * session.on('callDurationUpdate', function (event) {
3169
+ * document.getElementById('duration').textContent = event.durationStr;
3170
+ * });
3171
+ */
3172
+
3173
+ const self = this;
3174
+ if (!this._durationUpdTimerObj) {
3175
+ this._durationUpdTimerObj = setInterval(function () {
3176
+ const activeDuration = self._callAnswerTime ? Date.now() - self._callAnswerTime : null;
3177
+ const e = {
3178
+ uuid: self._callUUID,
3179
+ beginTime: self._callBeginTime,
3180
+ answerTime: self._callAnswerTime,
3181
+ endTime: self._callEndTime,
3182
+ duration: Date.now() - self._callBeginTime,
3183
+ durationStr: createDurationString(Date.now() - self._callBeginTime),
3184
+ activeDuration: activeDuration,
3185
+ activeDurationStr: activeDuration ? createDurationString(activeDuration) : "",
3186
+ };
3187
+
3188
+ try {
3189
+ self.emit("callDurationUpdate", e);
3190
+ } catch (e) {
3191
+ self.log.error("Exception in call duration update event: " + e);
3192
+ }
3193
+ }, 1000);
3194
+ }
3195
+ }
3196
+
3197
+ // Begin rtcp stats update timer events
3198
+ _startRTCPStatsUpdateTimer() {
3199
+ /**
3200
+ * @event callQualityReportUpdate
3201
+ * @description
3202
+ * Contains information about call quality (fires in interval of 5 sec).
3203
+ * Payload contains "total" (metrics for whole session) and "delta" (metrics last 5 secs).
3204
+ *
3205
+ * @type {object}
3206
+ * @property {String} uuid - UUID of session
3207
+ * @property {Number} outboundPacketsSent - Number of packets sent
3208
+ * @property {Number} outboundBytesSent - Number of bytes sent
3209
+ * @property {Number} outboundPacketsLost - Number of packets lost to remote
3210
+ * @property {Number} outboundPacketLossPct - Percentage of outbound packets lost
3211
+ * @property {Number} outboundJitter - Network jitter to remote (ms)
3212
+ * @property {Number} inboundPacketsReceived - Number of packets received
3213
+ * @property {Number} inboundBytesReceived - Number of bytes received
3214
+ * @property {Number} inboundPacketsLost - Number of packets lost from remote
3215
+ * @property {Number} inboundPacketLossPct - Percentage of inbound packets lost
3216
+ * @property {Number} inboundJitter - Network jitter from remote (ms)
3217
+ * @property {Number} roundTripTime - Latency in ms to media gateway
3218
+ * @property {Number} effectiveLatency - Effective latency with combined jitter impact
3219
+ * @property {Number} rFactor - Quality of media stream (0 - 100%)
3220
+ * @property {Number} mos - Synthetic MOS
3221
+ * @property {String} qualityString - Quality string [Excellent, Good, Fair, Poor, Bad]
3222
+ *
3223
+ * @example
3224
+ * // Update call quality text on screen
3225
+ * session.on('callQualityReportUpdate', (report) => {
3226
+ * document.getElementById('quality_text').textContent = report.delta.qualityString;
3227
+ * document.getElementById('latency').textContent = report.delta.effectiveLatency;
3228
+ * });
3229
+ */
3230
+
3231
+ if (!this._rtcpStatsTimer) {
3232
+ const emptyReport = {
3233
+ outboundPacketsSent: 0,
3234
+ outboundBytesSent: 0,
3235
+ outboundPacketsLost: 0,
3236
+ outboundJitter: 0,
3237
+ inboundPacketsReceived: 0,
3238
+ inboundBytesReceived: 0,
3239
+ inboundPacketsLost: 0,
3240
+ inboundJitter: 0,
3241
+ roundTripTime: 0,
3242
+ };
3243
+
3244
+ // Initialize report first
3245
+ this._currentReport = Object.assign({}, emptyReport);
3246
+
3247
+ this._rtcpStatsTimer = setInterval(() => {
3248
+ this._webRTCSession._connection.getStats().then((data) => {
3249
+ let totalReport = Object.assign({}, emptyReport);
3250
+ let deltaReport = Object.assign({}, emptyReport);
3251
+
3252
+ for (let report of data.values()) {
3253
+ // this.log.debug(report);
3254
+ switch (report.type) {
3255
+ case "outbound-rtp":
3256
+ // this.log.debug('Got: outbound-rtp');
3257
+ // this.log.debug(report);
3258
+ // Packets
3259
+ totalReport.outboundPacketsSent = report.packetsSent;
3260
+ deltaReport.outboundPacketsSent = report.packetsSent - this._currentReport.outboundPacketsSent;
3261
+ // Bytes
3262
+ totalReport.outboundBytesSent = report.bytesSent;
3263
+ deltaReport.outboundBytesSent = report.bytesSent - this._currentReport.outboundBytesSent;
3264
+ break;
3265
+
3266
+ case "inbound-rtp":
3267
+ // this.log.debug('Got: inbound-rtp');
3268
+ // this.log.debug(report);
3269
+ // Packets
3270
+ totalReport.inboundPacketsReceived = report.packetsReceived;
3271
+ deltaReport.inboundPacketsReceived = report.packetsReceived - this._currentReport.inboundPacketsReceived;
3272
+ // Bytes
3273
+ totalReport.inboundBytesReceived = report.bytesReceived;
3274
+ deltaReport.inboundBytesReceived = report.bytesReceived - this._currentReport.inboundBytesReceived;
3275
+ // Loss
3276
+ totalReport.inboundPacketsLost = report.packetsLost;
3277
+ deltaReport.inboundPacketsLost = report.packetsLost - this._currentReport.inboundPacketsLost;
3278
+ // Jitter
3279
+ totalReport.inboundJitter = report.jitter * 1000;
3280
+ deltaReport.inboundJitter = report.jitter * 1000;
3281
+ break;
3282
+
3283
+ case "remote-inbound-rtp":
3284
+ // this.log.debug('Got: remote-inbound-rtp');
3285
+ // this.log.debug(report);
3286
+ // Loss
3287
+ totalReport.outboundPacketsLost = report.packetsLost;
3288
+ deltaReport.outboundPacketsLost = report.packetsLost - this._currentReport.outboundPacketsLost;
3289
+ // Jitter
3290
+ totalReport.outboundJitter = report.jitter * 1000;
3291
+ deltaReport.outboundJitter = report.jitter * 1000;
3292
+ // RTT
3293
+ totalReport.roundTripTime = report.roundTripTime * 1000;
3294
+ deltaReport.roundTripTime = report.roundTripTime * 1000;
3295
+ break;
3296
+ }
3297
+ }
3298
+
3299
+ this._currentReport = this._calcReportMetrics(totalReport);
3300
+
3301
+ // We have to wait for RTT before firing the first event
3302
+ if (totalReport.roundTripTime) {
3303
+ // Do current report metrics
3304
+ const finalReport = {
3305
+ total: this._currentReport,
3306
+ delta: this._calcReportMetrics(deltaReport),
3307
+ };
3308
+
3309
+ try {
3310
+ this.emit("callQualityReportUpdate", finalReport);
3311
+ } catch (e) {
3312
+ this.log.error("Exception in call quality report update event: " + e);
3313
+ }
3314
+ }
3315
+ });
3316
+ }, 5000);
3317
+ }
3318
+ }
3319
+
3320
+ // Return quality string based on R-factor
3321
+ _calcReportMetrics(report) {
3322
+ // Packet loss
3323
+ report.outboundPacketLossPct = report.outboundPacketsLost / report.outboundPacketsSent;
3324
+ report.inboundPacketLossPct = report.inboundPacketsLost / report.inboundPacketsReceived;
3325
+
3326
+ // Effective latency
3327
+ report.effectiveLatency = report.roundTripTime + (report.inboundJitter + report.outboundJitter) + 10;
3328
+
3329
+ // R-Factor and mos
3330
+ const lossRate = (report.outboundPacketLossPct + report.inboundPacketLossPct) * 100;
3331
+ const lossEffect = (95.0 * lossRate) / (lossRate + 10);
3332
+ const latencyEffect = report.effectiveLatency * 0.1;
3333
+ // this.log.debug(`R factor loss effect ${lossEffect} / latency effect ${latencyEffect}`);
3334
+
3335
+ report.rFactor = Math.max(0, 100 - lossEffect - latencyEffect);
3336
+ report.mos = 1 + 0.035 * report.rFactor + 0.000007 * report.rFactor * (report.rFactor - 60) * (100 - report.rFactor);
3337
+
3338
+ // Quality string
3339
+ switch (true) {
3340
+ case report.rFactor < 50:
3341
+ report.qualityString = "Bad";
3342
+ break;
3343
+ case report.rFactor < 70:
3344
+ report.qualityString = "Poor";
3345
+ break;
3346
+ case report.rFactor < 80:
3347
+ report.qualityString = "Fair";
3348
+ break;
3349
+ case report.rFactor < 90:
3350
+ report.qualityString = "Good";
3351
+ break;
3352
+ case report.rFactor <= 100:
3353
+ report.qualityString = "Excellent";
3354
+ break;
3355
+ default:
3356
+ report.qualityString = "Error";
3357
+ }
3358
+
3359
+ return report;
3360
+ }
3361
+
3362
+ /**
3363
+ * @typedef SESSION_STATE {string}
3364
+ * @property {string} INITIALIZING Session is initializing (Gather ICE candidates)
3365
+ * @property {string} CALLING Sending INVITE to server
3366
+ * @property {string} PROGRESS Call progress received ('event.code' has new progress status)
3367
+ * @property {string} ACTIVE Call is active (answered)
3368
+ * @property {string} TERMINATED Call ended
3369
+ * @property {string} FAILED Call failed (see 'event.cause' for more info)
3370
+ * @property {string} ON_HOLD Call is on-hold
3371
+ * @property {string} INCOMING Incoming call is waiting
3372
+ * @property {string} PLAYBACK Playback with pending hangup in progress
3373
+ */
3374
+
3375
+ // Set session state and emit event
3376
+ _setSessionState(state, info = null) {
3377
+ /**
3378
+ * @event sessionStateChange
3379
+ * @type {object}
3380
+ * @param {SESSION_STATE} state - The current session state
3381
+ * @param {uuid} uuid - Call UUID for session
3382
+ * @param {string} originName - Originating name of call [INCOMING only]
3383
+ * @param {string} originatingNumber - Originating phone number of call [INCOMING only]
3384
+ * @param {string} destinationNumber - Destination number of call
3385
+ * @param {string} cause - Fail cause [FAILED only]
3386
+ * @param {string} message - Fail message description [FAILED only]
3387
+ *
3388
+ */
3389
+
3390
+ this.log.debug("Session state: " + state);
3391
+ if (info) {
3392
+ this.log.debug(info);
3393
+ }
3394
+
3395
+ this._sessionState = state;
3396
+ try {
3397
+ this.emit(
3398
+ "sessionStateChange",
3399
+ Object.assign(
3400
+ {
3401
+ state: state,
3402
+ uuid: this._callUUID,
3403
+ },
3404
+ info,
3405
+ ),
3406
+ );
3407
+ } catch (e) {
3408
+ this.log.error("Exception in sessionStateChange event:" + e);
3409
+ }
3410
+
3411
+ // Trigger callDataAvailable dummy event
3412
+ if (state === "TERMINATED") {
3413
+ this._callDisposition = this._manualDisposition || "NORMAL";
3414
+ this._callFinished();
3415
+ }
3416
+
3417
+ if (state === "FAILED") {
3418
+ if (info.cause === "Unavailable") {
3419
+ this._callDisposition = "TEMP_UNAVAIL";
3420
+ if (this._phone.options.playSignalTones) {
3421
+ this._phone._startRepeatTone(ST_TEMP_UNAVAIL);
3422
+ }
3423
+ }
3424
+
3425
+ if (info.cause === "Not Found") {
3426
+ this._callDisposition = "INVALID_NUMBER";
3427
+ if (this._phone.options.playSignalTones) {
3428
+ this._phone._startRepeatTone(ST_INVALID_NUMBER);
3429
+ }
3430
+ }
3431
+
3432
+ if (info.cause === "Canceled") {
3433
+ this._callDisposition = this._manualDisposition || "NO_ANSWER";
3434
+ }
3435
+
3436
+ if (info.cause === "Rejected") {
3437
+ if (this.direction === "INBOUND") {
3438
+ this._callDisposition = "REJECTED";
3439
+ } else {
3440
+ this._callDisposition = "BARRED";
3441
+ }
3442
+ }
3443
+
3444
+ if (info.cause === "Busy") {
3445
+ this._callDisposition = "BUSY";
3446
+ if (this._phone.options.playSignalTones) {
3447
+ this._phone._startRepeatTone(ST_BUSY);
3448
+ }
3449
+ }
3450
+
3451
+ // If session is incoming and failed, we don't fire calldataAvailable event
3452
+ if (this._webRTCSession.direction === "incoming") ;
3453
+
3454
+ this._callFinished();
3455
+ }
3456
+ }
3457
+
3458
+ /**
3459
+ * @typedef {string} CALL_DIRECTION
3460
+ * @property {string} INBOUND Incoming call
3461
+ * @property {string} OUTBOUND Outgoing call
3462
+ * @property {string} TRANSFER Transferred call
3463
+ * @property {string} LOCAL Local call
3464
+ * @property {string} AUTOMATED Call triggered from api / server
3465
+ * @property {string} LISTEN Listen call
3466
+ */
3467
+
3468
+ /**
3469
+ * @typedef {string} CALL_DISPOSITION
3470
+ * @property {string} BUSY Number was busy
3471
+ * @property {string} INVALID_NUMBER Number call does not exist
3472
+ * @property {string} NO_ANSWER Call was not picked up before cancelling
3473
+ * @property {string} TEMP_UNAVAIL Remote is temporarily unavailable
3474
+ * @property {string} NORMAL Call was terminated normally
3475
+ * @property {string} FAILED There was a failure connecting the call
3476
+ * @property {string} BARRED Call was prohibited by server
3477
+ * @property {string} REJECTED Incoming call was rejected
3478
+ */
3479
+ _callFinished() {
3480
+ this.log.info("Call finished");
3481
+
3482
+ // Fire callEnded event
3483
+ this._fireCallEndedEvent();
3484
+
3485
+ // Stop duration update timer
3486
+ clearInterval(this._durationUpdTimerObj);
3487
+
3488
+ // Stop rtcp stats uptime timer
3489
+ clearInterval(this._rtcpStatsTimer);
3490
+
3491
+ // Remove session from active sessions
3492
+ this._phone._sessionTerminated(this);
3493
+ }
3494
+
3495
+ // Set answer call timeout
3496
+ _setAnswerCallTimeout(timeout) {
3497
+ this.log.debug("Setting answer call timeout to: " + timeout);
3498
+ this._answerCallTimeout = timeout;
3499
+ }
3500
+
3501
+ // Fire recording state event
3502
+ _fireRecordingStateEvent() {
3503
+ /**
3504
+ * @event recordingStateChange
3505
+ * @type {object}
3506
+ * @property {boolean} state - New recording state
3507
+ */
3508
+ try {
3509
+ this.emit("recordingStateChange", { state: this._isRecording, error: this._recordingError });
3510
+ } catch (e) {
3511
+ this.log.error("Exception in recording state change event: " + e);
3512
+ }
3513
+ }
3514
+
3515
+ // Fire mic mute state event
3516
+ _fireMuteStateEvent() {
3517
+ /**
3518
+ * @event muteStateChange
3519
+ * @type {object}
3520
+ * @property {boolean} state - Mute state (true = muted / false = unmuted)
3521
+ */
3522
+ try {
3523
+ this.emit("muteStateChange", { state: this._isMuted });
3524
+ } catch (e) {
3525
+ this.log.error("Exception in mute state change event: " + e);
3526
+ }
3527
+ }
3528
+
3529
+ // Fire recording state event
3530
+ _fireHoldStateEvent() {
3531
+ /**
3532
+ * @event holdStateChange
3533
+ * @type {object}
3534
+ * @property {boolean} state - New hold state
3535
+ */
3536
+ try {
3537
+ this.emit("holdStateChange", { state: this._isOnHold });
3538
+ } catch (e) {
3539
+ this.log.error("Exception in hold state change event: " + e);
3540
+ }
3541
+ }
3542
+
3543
+ // Fire whisper mode event
3544
+ _fireWhisperModeChangeEvent() {
3545
+ /**
3546
+ * @event whisperModeChange
3547
+ * @type {object}
3548
+ * @property {boolean} mode - New whisper mode (0 = No whisper / 1 = Local only / 2 = Three way)
3549
+ */
3550
+ try {
3551
+ this.emit("whisperModeChange", { mode: this._whisperMode });
3552
+ } catch (e) {
3553
+ this.log.error("Exception in whisper mode change event: " + e);
3554
+ }
3555
+ }
3556
+
3557
+ // Fire call data event to subscribers
3558
+ _fireCallEndedEvent() {
3559
+ /**
3560
+ * @event callEnded
3561
+ *
3562
+ * @type {object}
3563
+ * @property {uuid} uuid - Customer Call UUID for session
3564
+ * @param {CALL_DIRECTION} direction - Call direction
3565
+ * @param {CALL_DISPOSITION} disposition - The final call disposition
3566
+ * @param {string} originatingNumber - Caller ID number (a-number) used for call
3567
+ * @param {string} destinationNumber - Destination number (b-number) used for call
3568
+ * @param {Date} startTime - Start time of call
3569
+ * @param {Date} answerTime - Time call was answered
3570
+ * @param {Date} endTime - End time of call
3571
+ * @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
3572
+ * @param {String} durationStr - Total duration of the call formatted as (mm:ss)
3573
+ * @param {Number} answerDuration - Time before call was answered (millisec)
3574
+ * @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
3575
+ * @param {Number} activeDuration - Active duration of the call (time spend talking) (millisec)
3576
+ * @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
3577
+ * @param {object} callQualityReport - Call quality report (see callQualityReportUpdate event for details)
3578
+ */
3579
+
3580
+ let e;
3581
+
3582
+ e = {
3583
+ uuid: this._callUUID,
3584
+ direction: this.direction,
3585
+ disposition: this._callDisposition,
3586
+ originatingNumber: this._originatingNumber,
3587
+ destinationNumber: this._destinationNumber,
3588
+
3589
+ // Calc durations
3590
+ duration: this._callEndTime - this._callBeginTime,
3591
+ answerDuration: this._callAnswerTime ? this._callAnswerTime - this._callBeginTime : null,
3592
+ activeDuration: this._callAnswerTime ? this._callEndTime - this._callAnswerTime : null,
3593
+
3594
+ // Convert timestamp into ISO8601
3595
+ startTime: new Date(this._callBeginTime).toISOString(),
3596
+ endTime: new Date(this._callEndTime).toISOString(),
3597
+ answerTime: this._callAnswerTime ? new Date(this._callAnswerTime).toISOString() : null,
3598
+ };
3599
+
3600
+ // Add some extra convenience fields
3601
+ e.durationStr = createDurationString(e.duration);
3602
+ e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) : "";
3603
+ e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) : "";
3604
+ e.callQualityReport = this._currentReport;
3605
+
3606
+ try {
3607
+ this.emit("callEnded", e);
3608
+ } catch (e) {
3609
+ this.log.error("Exception in callEnded event: " + e);
3610
+ }
3611
+ }
3612
+
3613
+ // Setup handling of remote audio
3614
+ _handleRemoteAudio(session) {
3615
+ // Stop any repeating tones if audio comes
3616
+ this._phone._endRepeatTone();
3617
+
3618
+ const self = this;
3619
+ session.connection.ontrack = function (event) {
3620
+ self.log.debug("Start media output (track):");
3621
+ self.log.debug(event);
3622
+ self._phone._remoteAudioElement.srcObject = event.streams[0];
3623
+ };
3624
+ }
3625
+ }
3626
+
3627
+ export { EventClient, InvalidMediaDevice, InvalidOptions, InvalidParameter, LOGLVL$1 as LOGLVL, LogHandler, MediaNotPrepared, NoActiveSession, NotAllowed, NotReady, Phone, Request, Requestor, Session, UnableToConnect, browserIsChrome, browserIsEdge, browserIsFirefox, browserIsOpera, browserIsSafari, compareObjects, createDurationString, createUUID };