@telegenta/webclient 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demos/calldata_available.html +1 -1
- package/demos/event_client.html +225 -210
- package/demos/phone_client.html +539 -519
- package/dist/telegenta.js +410 -292
- package/dist/telegenta.min.js +1 -1
- package/docs/EventClient.html +1 -1
- package/docs/EventServiceNotConnected.html +3 -0
- package/docs/InvalidMediaDevice.html +1 -1
- package/docs/InvalidOptions.html +1 -1
- package/docs/InvalidParameter.html +1 -1
- package/docs/LogHandler.html +2 -2
- package/docs/MediaNotPrepared.html +1 -1
- package/docs/NoActiveSession.html +1 -1
- package/docs/NotAllowed.html +1 -1
- package/docs/NotReady.html +1 -1
- package/docs/Phone.html +2 -2
- package/docs/Request.html +1 -1
- package/docs/Requestor.html +1 -1
- package/docs/Session.html +4 -1
- package/docs/UnableToConnect.html +1 -1
- package/docs/data/search.json +1 -1
- package/docs/event-client.js.html +44 -38
- package/docs/exceptions.js.html +15 -4
- package/docs/global.html +3 -3
- package/docs/index.html +1 -1
- package/docs/logger.js.html +2 -2
- package/docs/phone-client.js.html +328 -264
- package/docs/requestor.js.html +3 -3
- package/package.json +45 -45
package/dist/telegenta.js
CHANGED
|
@@ -168,10 +168,11 @@ class InvalidParameter extends Error {
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Exception raised if trying to perform actions that require active session
|
|
171
|
+
* @property message - Error message
|
|
171
172
|
*/
|
|
172
173
|
class NoActiveSession extends Error {
|
|
173
|
-
constructor() {
|
|
174
|
-
super();
|
|
174
|
+
constructor(message) {
|
|
175
|
+
super(message);
|
|
175
176
|
this.name = 'NoActiveSession';
|
|
176
177
|
}
|
|
177
178
|
}
|
|
@@ -197,6 +198,16 @@ class NotAllowed extends Error {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Exception raised if event service is not connected
|
|
203
|
+
*/
|
|
204
|
+
class EventServiceNotConnected extends Error {
|
|
205
|
+
constructor() {
|
|
206
|
+
super();
|
|
207
|
+
this.name = 'EventServiceNotConnected';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
/*
|
|
201
212
|
* TOOLBOX
|
|
202
213
|
*/
|
|
@@ -281,6 +292,43 @@ function browserIsEdge() {
|
|
|
281
292
|
return (navigator.userAgent.indexOf("Edge") !== -1)
|
|
282
293
|
}
|
|
283
294
|
|
|
295
|
+
// SIP header names are RFC 3261 tokens
|
|
296
|
+
const HEADER_NAME_RE = /^[A-Za-z0-9\-.!%*_+`'~]+$/;
|
|
297
|
+
|
|
298
|
+
const HTML_ENTITIES = {
|
|
299
|
+
'&': '&',
|
|
300
|
+
'<': '<',
|
|
301
|
+
'>': '>',
|
|
302
|
+
'"': '"',
|
|
303
|
+
"'": ''',
|
|
304
|
+
'\r': ' ',
|
|
305
|
+
'\n': ' ',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Build custom SIP header lines from a { name: value } map.
|
|
309
|
+
// Names must be tokens - a bad one is always a programming error, so it throws.
|
|
310
|
+
// Values are HTML encoded rather than rejected: that strips the CR/LF which
|
|
311
|
+
// would otherwise let a value inject further headers, while still letting
|
|
312
|
+
// punctuation through.
|
|
313
|
+
function buildCustomHeaders(setHeader) {
|
|
314
|
+
if (!setHeader) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (typeof setHeader !== 'object' || Array.isArray(setHeader)) {
|
|
319
|
+
throw new TypeError(`Invalid setHeader argument: expected an object, got ${typeof setHeader}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return Object.entries(setHeader).map(([name, value]) => {
|
|
323
|
+
if (!HEADER_NAME_RE.test(name)) {
|
|
324
|
+
throw new TypeError(`Invalid SIP header name: "${name}"`);
|
|
325
|
+
}
|
|
326
|
+
// Single pass, so an encoded '&' is not encoded again.
|
|
327
|
+
const encoded = String(value).replace(/[&<>"'\r\n]/g, (c) => HTML_ENTITIES[c]);
|
|
328
|
+
return `${name}: ${encoded}`;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
284
332
|
// Request timeout after 10 sec
|
|
285
333
|
const REQUEST_TIMEOUT = 10000;
|
|
286
334
|
|
|
@@ -444,7 +492,7 @@ class Requestor extends EventEmitter {
|
|
|
444
492
|
resolve: resolve,
|
|
445
493
|
reject: reject,
|
|
446
494
|
timeoutObj: setTimeout(function () {
|
|
447
|
-
self.log.
|
|
495
|
+
self.log.info(`Timeout on request ${r.id}, rejecting!`);
|
|
448
496
|
self.removePendingRequest(r.id);
|
|
449
497
|
reject("Request timed out");
|
|
450
498
|
}, REQUEST_TIMEOUT),
|
|
@@ -512,7 +560,7 @@ class Requestor extends EventEmitter {
|
|
|
512
560
|
// Keep alive in seconds
|
|
513
561
|
const PING_INTERVAL = 50;
|
|
514
562
|
|
|
515
|
-
const ES_DEFAULT_SERVER_URL =
|
|
563
|
+
const ES_DEFAULT_SERVER_URL = 'wss://esvc.telegenta.com/';
|
|
516
564
|
|
|
517
565
|
// Starting retry interval
|
|
518
566
|
const RETRY_INTERVAL_START = 1 + Math.random();
|
|
@@ -550,7 +598,7 @@ class EventClient {
|
|
|
550
598
|
this.log = logger ? logger : new LogHandler(LOGLVL.ERROR);
|
|
551
599
|
this._requestor = new Requestor(this.log);
|
|
552
600
|
|
|
553
|
-
this._requestor.on(
|
|
601
|
+
this._requestor.on('event', (request) => {
|
|
554
602
|
this._handleIncomingEvent(request);
|
|
555
603
|
});
|
|
556
604
|
}
|
|
@@ -573,11 +621,11 @@ class EventClient {
|
|
|
573
621
|
|
|
574
622
|
/**
|
|
575
623
|
* Disconnect from event service and clear all active subscriptions
|
|
576
|
-
* @throws
|
|
624
|
+
* @throws EventServiceNotConnected if not connected
|
|
577
625
|
*/
|
|
578
626
|
disconnect() {
|
|
579
627
|
if (this._socket.readyState !== 1) {
|
|
580
|
-
throw new
|
|
628
|
+
throw new EventServiceNotConnected();
|
|
581
629
|
}
|
|
582
630
|
|
|
583
631
|
this._activeSubscriptions = [];
|
|
@@ -602,26 +650,29 @@ class EventClient {
|
|
|
602
650
|
* @param eventSpecification object Event parameters (please check docs for further description)
|
|
603
651
|
* @param callback object Callback function (will be called when event is received)
|
|
604
652
|
* @throws InvalidParameter
|
|
653
|
+
* @throws EventServiceNotConnected
|
|
605
654
|
* @returns object Promise on subscription
|
|
606
655
|
*/
|
|
607
656
|
subscribe(eventSpecification, callback) {
|
|
608
657
|
if (!eventSpecification || !callback) {
|
|
609
|
-
throw new InvalidParameter(
|
|
658
|
+
throw new InvalidParameter('You must specify both event spec and callback function');
|
|
610
659
|
}
|
|
611
660
|
|
|
612
|
-
if (!eventSpecification.hasOwnProperty(
|
|
613
|
-
throw new InvalidParameter(
|
|
661
|
+
if (!eventSpecification.hasOwnProperty('event_name')) {
|
|
662
|
+
throw new InvalidParameter('Your event specification must contain event name');
|
|
614
663
|
}
|
|
615
664
|
|
|
616
665
|
if (!this.isConnected()) {
|
|
617
|
-
throw new
|
|
666
|
+
throw new EventServiceNotConnected();
|
|
618
667
|
}
|
|
619
668
|
|
|
620
669
|
const self = this;
|
|
621
670
|
return new Promise(function (resolve, reject) {
|
|
622
|
-
self._requestor.sendRequest(self._socket,
|
|
671
|
+
self._requestor.sendRequest(self._socket, 'subscribe', eventSpecification).then(
|
|
623
672
|
function (subscriptionId) {
|
|
624
|
-
self.log.debug(
|
|
673
|
+
self.log.debug(
|
|
674
|
+
`Subscription to '${eventSpecification.event_name}' was successful, ID = ${subscriptionId}`,
|
|
675
|
+
);
|
|
625
676
|
self._activeSubscriptions.push(new EventSubscription(subscriptionId, eventSpecification, callback));
|
|
626
677
|
resolve(subscriptionId);
|
|
627
678
|
},
|
|
@@ -637,22 +688,23 @@ class EventClient {
|
|
|
637
688
|
* Unsubscribe event messages
|
|
638
689
|
* @param subscriptionId Object Event specification to unsubscribe (must match specification used on subscribe)
|
|
639
690
|
* @throws InvalidParameter
|
|
691
|
+
* @throws EventServiceNotConnected
|
|
640
692
|
* @returns object Promise on unsubscribe
|
|
641
693
|
*/
|
|
642
694
|
unsubscribe(subscriptionId) {
|
|
643
695
|
if (!subscriptionId) {
|
|
644
|
-
throw new InvalidParameter(
|
|
696
|
+
throw new InvalidParameter('You must specify subscription id');
|
|
645
697
|
}
|
|
646
698
|
|
|
647
699
|
if (!this.isConnected()) {
|
|
648
|
-
throw new
|
|
700
|
+
throw new EventServiceNotConnected();
|
|
649
701
|
}
|
|
650
702
|
|
|
651
|
-
this.log.info(
|
|
703
|
+
this.log.info('Unsubscribe id: ' + subscriptionId);
|
|
652
704
|
|
|
653
705
|
const self = this;
|
|
654
706
|
return new Promise(function (resolve, reject) {
|
|
655
|
-
self._requestor.sendRequest(self._socket,
|
|
707
|
+
self._requestor.sendRequest(self._socket, 'unsubscribe', { id: subscriptionId }).then(
|
|
656
708
|
(data) => {
|
|
657
709
|
self.log.info(`Unsubscribe to '${subscriptionId}' was successful`);
|
|
658
710
|
self._removeSubscription(subscriptionId);
|
|
@@ -670,12 +722,12 @@ class EventClient {
|
|
|
670
722
|
* Fire event message
|
|
671
723
|
* @param type Event type to fire
|
|
672
724
|
* @param payload Event payload
|
|
673
|
-
* @throws
|
|
725
|
+
* @throws EventServiceNotConnected
|
|
674
726
|
* @returns object Promise on event
|
|
675
727
|
*/
|
|
676
728
|
fireEvent(type, payload) {
|
|
677
729
|
if (!this.isConnected()) {
|
|
678
|
-
throw new
|
|
730
|
+
throw new EventServiceNotConnected();
|
|
679
731
|
}
|
|
680
732
|
|
|
681
733
|
return new Promise((resolve, reject) => {
|
|
@@ -708,20 +760,20 @@ class EventClient {
|
|
|
708
760
|
|
|
709
761
|
// Connect to event service
|
|
710
762
|
_connectServer() {
|
|
711
|
-
this.log.info(
|
|
763
|
+
this.log.info('Connecting to event server: ' + this._serverAddress);
|
|
712
764
|
|
|
713
765
|
const self = this;
|
|
714
766
|
return new Promise(function (resolve, reject) {
|
|
715
767
|
if (self._socket) {
|
|
716
768
|
switch (self._socket.readyState) {
|
|
717
769
|
case 0:
|
|
718
|
-
reject(
|
|
770
|
+
reject('Unable to connect, already connecting');
|
|
719
771
|
return;
|
|
720
772
|
case 1:
|
|
721
|
-
reject(
|
|
773
|
+
reject('Unable to connect, already connected');
|
|
722
774
|
return;
|
|
723
775
|
case 2:
|
|
724
|
-
reject(
|
|
776
|
+
reject('Unable to connect, socket is still closing');
|
|
725
777
|
return;
|
|
726
778
|
}
|
|
727
779
|
}
|
|
@@ -730,8 +782,8 @@ class EventClient {
|
|
|
730
782
|
let was_connected = false;
|
|
731
783
|
|
|
732
784
|
// CONNECTION OPEN
|
|
733
|
-
self._socket.addEventListener(
|
|
734
|
-
self.log.info(
|
|
785
|
+
self._socket.addEventListener('open', function (/*event*/) {
|
|
786
|
+
self.log.info('Successfully connected to event service');
|
|
735
787
|
|
|
736
788
|
was_connected = true;
|
|
737
789
|
// After successful connect we enable auto connect and reset reconnect timer
|
|
@@ -739,7 +791,7 @@ class EventClient {
|
|
|
739
791
|
self._reconnectRetrySeconds = RETRY_INTERVAL_START;
|
|
740
792
|
|
|
741
793
|
// Hook up event for incoming messages to requestor
|
|
742
|
-
self._socket.addEventListener(
|
|
794
|
+
self._socket.addEventListener('message', function (event) {
|
|
743
795
|
self._requestor.receiveMessage(self._socket, event.data);
|
|
744
796
|
});
|
|
745
797
|
|
|
@@ -751,7 +803,7 @@ class EventClient {
|
|
|
751
803
|
// self.log.debug('Pong');
|
|
752
804
|
},
|
|
753
805
|
function (reason) {
|
|
754
|
-
self.log.warning(
|
|
806
|
+
self.log.warning('Ping failed: ' + reason);
|
|
755
807
|
self._reconnectServer();
|
|
756
808
|
},
|
|
757
809
|
);
|
|
@@ -772,19 +824,19 @@ class EventClient {
|
|
|
772
824
|
});
|
|
773
825
|
|
|
774
826
|
// CONNECTION CLOSE
|
|
775
|
-
self._socket.addEventListener(
|
|
776
|
-
self.log.info(
|
|
827
|
+
self._socket.addEventListener('close', function (/*event*/) {
|
|
828
|
+
self.log.info('Disconnected from event service');
|
|
777
829
|
clearTimeout(self._pingTimerObj);
|
|
778
830
|
// We try to reconnect if possible
|
|
779
831
|
self._reconnectServer();
|
|
780
832
|
});
|
|
781
833
|
|
|
782
834
|
// CONNECTION ERROR
|
|
783
|
-
self._socket.addEventListener(
|
|
784
|
-
self.log.error(
|
|
835
|
+
self._socket.addEventListener('error', function (error) {
|
|
836
|
+
self.log.error('Websocket connection error');
|
|
785
837
|
// Only reject promise if no connection was made (in that case promise is already resolved)
|
|
786
838
|
if (!was_connected) {
|
|
787
|
-
reject(
|
|
839
|
+
reject('Unable to connect: ' + error);
|
|
788
840
|
}
|
|
789
841
|
});
|
|
790
842
|
});
|
|
@@ -798,10 +850,10 @@ class EventClient {
|
|
|
798
850
|
setTimeout(function () {
|
|
799
851
|
self._connectServer().then(
|
|
800
852
|
function () {
|
|
801
|
-
self.log.info(
|
|
853
|
+
self.log.info('Reconnect attempt successful');
|
|
802
854
|
},
|
|
803
855
|
function () {
|
|
804
|
-
self.log.warning(
|
|
856
|
+
self.log.warning('Reconnect attempt failed');
|
|
805
857
|
},
|
|
806
858
|
);
|
|
807
859
|
if (self._reconnectRetrySeconds <= RETRY_MAX_BACKOFF) {
|
|
@@ -809,7 +861,7 @@ class EventClient {
|
|
|
809
861
|
}
|
|
810
862
|
}, self._reconnectRetrySeconds * 1000);
|
|
811
863
|
} else {
|
|
812
|
-
this.log.info(
|
|
864
|
+
this.log.info('Auto reconnect disabled (disconnecting?)');
|
|
813
865
|
}
|
|
814
866
|
}
|
|
815
867
|
|
|
@@ -818,7 +870,9 @@ class EventClient {
|
|
|
818
870
|
const subId = request.data.subscription_id;
|
|
819
871
|
for (let sub of this._activeSubscriptions) {
|
|
820
872
|
if (sub.id === subId) {
|
|
821
|
-
this.log.debug(
|
|
873
|
+
this.log.debug(
|
|
874
|
+
`Received event for subcription id ${sub.id} payload: ` + JSON.stringify(request.data.payload),
|
|
875
|
+
);
|
|
822
876
|
try {
|
|
823
877
|
sub.callback(request.data.payload);
|
|
824
878
|
} catch (e) {
|
|
@@ -1487,8 +1541,8 @@ function camelcaseKeys(input, options) {
|
|
|
1487
1541
|
// 'use strict';
|
|
1488
1542
|
|
|
1489
1543
|
const options = {
|
|
1490
|
-
wssServer:
|
|
1491
|
-
eventServer:
|
|
1544
|
+
wssServer: 'wss://api.telegenta.com/sip/',
|
|
1545
|
+
eventServer: 'wss://esvc.telegenta.com/',
|
|
1492
1546
|
provisionToken: null,
|
|
1493
1547
|
authorizationUsername: null,
|
|
1494
1548
|
password: null,
|
|
@@ -1521,9 +1575,9 @@ const DTMF_FREQUENCY_TABLE = {
|
|
|
1521
1575
|
8: [1336, 852],
|
|
1522
1576
|
9: [1477, 852],
|
|
1523
1577
|
c: [1633, 852],
|
|
1524
|
-
|
|
1578
|
+
'*': [1209, 941],
|
|
1525
1579
|
0: [1336, 941],
|
|
1526
|
-
|
|
1580
|
+
'#': [1477, 941],
|
|
1527
1581
|
d: [1633, 941],
|
|
1528
1582
|
};
|
|
1529
1583
|
|
|
@@ -1594,11 +1648,11 @@ class Phone extends EventEmitter {
|
|
|
1594
1648
|
this._activeSessions = {};
|
|
1595
1649
|
|
|
1596
1650
|
// Create remote audio element
|
|
1597
|
-
this._remoteAudioElement = document.createElement(
|
|
1651
|
+
this._remoteAudioElement = document.createElement('AUDIO');
|
|
1598
1652
|
this._remoteAudioElement.autoplay = true;
|
|
1599
1653
|
|
|
1600
1654
|
// Tone generator
|
|
1601
|
-
this._toneAudioElement = document.createElement(
|
|
1655
|
+
this._toneAudioElement = document.createElement('AUDIO');
|
|
1602
1656
|
this._toneAudioElement.autoplay = true;
|
|
1603
1657
|
this._toneTimer = null;
|
|
1604
1658
|
this._repeatToneActive = false;
|
|
@@ -1613,9 +1667,9 @@ class Phone extends EventEmitter {
|
|
|
1613
1667
|
|
|
1614
1668
|
// Handle jssip debug logs
|
|
1615
1669
|
if (this.log.level === LOGLVL$1.DEBUG) {
|
|
1616
|
-
JsSIP.debug.disable(
|
|
1670
|
+
JsSIP.debug.disable('JsSIP:*');
|
|
1617
1671
|
} else {
|
|
1618
|
-
JsSIP.debug.disable(
|
|
1672
|
+
JsSIP.debug.disable('JsSIP:*');
|
|
1619
1673
|
}
|
|
1620
1674
|
}
|
|
1621
1675
|
|
|
@@ -1627,7 +1681,7 @@ class Phone extends EventEmitter {
|
|
|
1627
1681
|
*
|
|
1628
1682
|
*/
|
|
1629
1683
|
prepareMedia() {
|
|
1630
|
-
this.log.info(
|
|
1684
|
+
this.log.info('Get media permission');
|
|
1631
1685
|
const self = this;
|
|
1632
1686
|
const constraints = { audio: true, video: false };
|
|
1633
1687
|
return new Promise(function (resolve, reject) {
|
|
@@ -1635,13 +1689,13 @@ class Phone extends EventEmitter {
|
|
|
1635
1689
|
() => {
|
|
1636
1690
|
self._mediaPermission = true;
|
|
1637
1691
|
self._mediaPermissionError = false;
|
|
1638
|
-
self.log.info(
|
|
1692
|
+
self.log.info('Media permission granted');
|
|
1639
1693
|
resolve();
|
|
1640
1694
|
},
|
|
1641
1695
|
(err) => {
|
|
1642
1696
|
self._mediaPermission = false;
|
|
1643
1697
|
self._mediaPermissionError = true;
|
|
1644
|
-
self.log.error(
|
|
1698
|
+
self.log.error('Denied media permission: ' + err);
|
|
1645
1699
|
reject(err);
|
|
1646
1700
|
},
|
|
1647
1701
|
);
|
|
@@ -1676,7 +1730,7 @@ class Phone extends EventEmitter {
|
|
|
1676
1730
|
this._checkMediaPermission();
|
|
1677
1731
|
// Check phone is not running already, if so stop it first
|
|
1678
1732
|
if (this._isStarted) {
|
|
1679
|
-
this.log.warning(
|
|
1733
|
+
this.log.warning('Phone already started, stopping first');
|
|
1680
1734
|
this.stop();
|
|
1681
1735
|
}
|
|
1682
1736
|
|
|
@@ -1691,29 +1745,29 @@ class Phone extends EventEmitter {
|
|
|
1691
1745
|
try {
|
|
1692
1746
|
pt = JSON.parse(atob(this._options.provisionToken));
|
|
1693
1747
|
} catch (err) {
|
|
1694
|
-
throw new InvalidOptions(
|
|
1748
|
+
throw new InvalidOptions('provisionToken', 'Invalid token');
|
|
1695
1749
|
}
|
|
1696
1750
|
|
|
1697
|
-
this.log.info(
|
|
1751
|
+
this.log.info('Using provisioning token authorization');
|
|
1698
1752
|
this._options.wssServer = pt.wss_server;
|
|
1699
1753
|
this._options.eventServer = pt.event_server;
|
|
1700
1754
|
this._options.authorizationUsername = pt.wss_username;
|
|
1701
1755
|
this._options.password = pt.wss_password;
|
|
1702
1756
|
this._options.callToken = pt.wss_token;
|
|
1703
1757
|
} else {
|
|
1704
|
-
this.log.info(
|
|
1758
|
+
this.log.info('Using manual specified credentials');
|
|
1705
1759
|
|
|
1706
1760
|
// Check auth info is provided
|
|
1707
1761
|
if (!this._options.authorizationUsername) {
|
|
1708
|
-
throw new InvalidOptions(
|
|
1762
|
+
throw new InvalidOptions('authorizationUsername', 'Missing username');
|
|
1709
1763
|
}
|
|
1710
1764
|
|
|
1711
1765
|
if (!this._options.password) {
|
|
1712
|
-
throw new InvalidOptions(
|
|
1766
|
+
throw new InvalidOptions('password', 'Missing password');
|
|
1713
1767
|
}
|
|
1714
1768
|
|
|
1715
1769
|
if (!this._options.callToken) {
|
|
1716
|
-
throw new InvalidOptions(
|
|
1770
|
+
throw new InvalidOptions('callToken', 'Missing call token');
|
|
1717
1771
|
}
|
|
1718
1772
|
}
|
|
1719
1773
|
|
|
@@ -1736,18 +1790,18 @@ class Phone extends EventEmitter {
|
|
|
1736
1790
|
if (this._options.useCallDataInfoEvent) {
|
|
1737
1791
|
// Subscribe to calldata_available events
|
|
1738
1792
|
let eventSpec = {
|
|
1739
|
-
event_name:
|
|
1793
|
+
event_name: 'calldata_available',
|
|
1740
1794
|
call_token: this._options.callToken,
|
|
1741
1795
|
};
|
|
1742
1796
|
|
|
1743
1797
|
this._eventClient
|
|
1744
1798
|
.subscribe(eventSpec, (data) => {
|
|
1745
1799
|
try {
|
|
1746
|
-
this.log.debug(
|
|
1800
|
+
this.log.debug('Received remote call data info');
|
|
1747
1801
|
this.log.debug(data);
|
|
1748
1802
|
this._fireCallDataInfoEvent(data);
|
|
1749
1803
|
} catch (e) {
|
|
1750
|
-
this.log.error(
|
|
1804
|
+
this.log.error('Event callback failed: ' + e);
|
|
1751
1805
|
}
|
|
1752
1806
|
})
|
|
1753
1807
|
.then(
|
|
@@ -1770,10 +1824,10 @@ class Phone extends EventEmitter {
|
|
|
1770
1824
|
try {
|
|
1771
1825
|
wsSocket = new JsSIP.WebSocketInterface(this._options.wssServer);
|
|
1772
1826
|
} catch (err) {
|
|
1773
|
-
throw new UnableToConnect(
|
|
1827
|
+
throw new UnableToConnect('Unable to connect phone websocket: ' + err);
|
|
1774
1828
|
}
|
|
1775
1829
|
|
|
1776
|
-
this.log.info(
|
|
1830
|
+
this.log.info('Starting phone');
|
|
1777
1831
|
this.log.debug(this._options);
|
|
1778
1832
|
|
|
1779
1833
|
this._UA = new JsSIP.UA({
|
|
@@ -1782,15 +1836,15 @@ class Phone extends EventEmitter {
|
|
|
1782
1836
|
password: this._options.password,
|
|
1783
1837
|
register: this._options.register,
|
|
1784
1838
|
display_name: this._options.authorizationUsername,
|
|
1785
|
-
uri: this._options.authorizationUsername +
|
|
1839
|
+
uri: this._options.authorizationUsername + '@telegenta.com',
|
|
1786
1840
|
session_timers: false,
|
|
1787
1841
|
});
|
|
1788
1842
|
|
|
1789
1843
|
// Setup remote media handler
|
|
1790
1844
|
const self = this;
|
|
1791
|
-
this._UA.on(
|
|
1792
|
-
self.log.debug(
|
|
1793
|
-
if (data.originator ===
|
|
1845
|
+
this._UA.on('newRTCSession', function (data) {
|
|
1846
|
+
self.log.debug('New RTCSession');
|
|
1847
|
+
if (data.originator === 'remote') {
|
|
1794
1848
|
// Create new session
|
|
1795
1849
|
const session = new Session(self);
|
|
1796
1850
|
|
|
@@ -1803,18 +1857,20 @@ class Phone extends EventEmitter {
|
|
|
1803
1857
|
// Check if we have other sessions and reject call if autoBusyIncoming is true
|
|
1804
1858
|
if (self._options.autoBusyIncoming) {
|
|
1805
1859
|
if (Object.keys(self._activeSessions).length > 0) {
|
|
1806
|
-
self.log.info(
|
|
1807
|
-
|
|
1860
|
+
self.log.info(
|
|
1861
|
+
`Autorejecting incoming call ${session.uuid} because there is already active calls`,
|
|
1862
|
+
);
|
|
1863
|
+
session.reject(486, 'Busy');
|
|
1808
1864
|
return;
|
|
1809
1865
|
} else {
|
|
1810
|
-
self.log.debug(
|
|
1866
|
+
self.log.debug('There are no other sessions, adding incoming call');
|
|
1811
1867
|
}
|
|
1812
1868
|
}
|
|
1813
1869
|
|
|
1814
1870
|
self._addSession(session);
|
|
1815
1871
|
|
|
1816
1872
|
// Set new session state
|
|
1817
|
-
session._setSessionState(
|
|
1873
|
+
session._setSessionState('INCOMING');
|
|
1818
1874
|
|
|
1819
1875
|
// Now fire incomingCall event
|
|
1820
1876
|
self._fireIncomingCallEvent(session);
|
|
@@ -1822,33 +1878,33 @@ class Phone extends EventEmitter {
|
|
|
1822
1878
|
});
|
|
1823
1879
|
|
|
1824
1880
|
// Setup connection state event subscriptions
|
|
1825
|
-
this._UA.on(
|
|
1826
|
-
self._setConnectionState(
|
|
1881
|
+
this._UA.on('connecting', function (/* data */) {
|
|
1882
|
+
self._setConnectionState('CONNECTING');
|
|
1827
1883
|
});
|
|
1828
1884
|
|
|
1829
|
-
this._UA.on(
|
|
1830
|
-
self._setConnectionState(
|
|
1885
|
+
this._UA.on('connected', function (/* data */) {
|
|
1886
|
+
self._setConnectionState('CONNECTED');
|
|
1831
1887
|
});
|
|
1832
1888
|
|
|
1833
|
-
this._UA.on(
|
|
1834
|
-
self._setConnectionState(
|
|
1889
|
+
this._UA.on('disconnected', function (/* data */) {
|
|
1890
|
+
self._setConnectionState('DISCONNECTED');
|
|
1835
1891
|
});
|
|
1836
1892
|
|
|
1837
|
-
this._UA.on(
|
|
1838
|
-
self._setConnectionState(
|
|
1893
|
+
this._UA.on('registered', function (/* data */) {
|
|
1894
|
+
self._setConnectionState('REGISTERED');
|
|
1839
1895
|
});
|
|
1840
1896
|
|
|
1841
|
-
this._UA.on(
|
|
1842
|
-
self._setConnectionState(
|
|
1897
|
+
this._UA.on('unregistered', function (/* data */) {
|
|
1898
|
+
self._setConnectionState('UNREGISTERED');
|
|
1843
1899
|
});
|
|
1844
1900
|
|
|
1845
|
-
this._UA.on(
|
|
1846
|
-
self._setConnectionState(
|
|
1901
|
+
this._UA.on('registrationFailed', function (/* data */) {
|
|
1902
|
+
self._setConnectionState('REGISTRATION_FAILED');
|
|
1847
1903
|
});
|
|
1848
1904
|
|
|
1849
1905
|
this._UA.start();
|
|
1850
1906
|
this._isStarted = true;
|
|
1851
|
-
this.log.info(
|
|
1907
|
+
this.log.info('Successfully started phone');
|
|
1852
1908
|
}
|
|
1853
1909
|
|
|
1854
1910
|
/**
|
|
@@ -1858,7 +1914,7 @@ class Phone extends EventEmitter {
|
|
|
1858
1914
|
stop() {
|
|
1859
1915
|
// Check phone is started first
|
|
1860
1916
|
if (!this._isStarted) {
|
|
1861
|
-
throw new NotReady(
|
|
1917
|
+
throw new NotReady('Phone is not started');
|
|
1862
1918
|
}
|
|
1863
1919
|
|
|
1864
1920
|
// Make sure events are not firing anymore, we can't rely on GC.
|
|
@@ -1870,18 +1926,18 @@ class Phone extends EventEmitter {
|
|
|
1870
1926
|
|
|
1871
1927
|
// Disconnect event client if needed
|
|
1872
1928
|
if (!this._eventClient) {
|
|
1873
|
-
this.log.debug(
|
|
1929
|
+
this.log.debug('Event client not running, not stopping');
|
|
1874
1930
|
return;
|
|
1875
1931
|
}
|
|
1876
1932
|
try {
|
|
1877
1933
|
this._eventClient.disconnect();
|
|
1878
1934
|
delete this._eventClient;
|
|
1879
1935
|
} catch (e) {
|
|
1880
|
-
this.log.warning(
|
|
1936
|
+
this.log.warning('Unable to disconnect event client');
|
|
1881
1937
|
}
|
|
1882
1938
|
|
|
1883
1939
|
this._isStarted = false;
|
|
1884
|
-
this.log.info(
|
|
1940
|
+
this.log.info('Phone client successfully stopped');
|
|
1885
1941
|
}
|
|
1886
1942
|
|
|
1887
1943
|
/**
|
|
@@ -1895,20 +1951,22 @@ class Phone extends EventEmitter {
|
|
|
1895
1951
|
* @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
1952
|
* @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
1953
|
* @param {string} [params.metaInfo] Add meta info to call, which will be available in server events and inside call complete webhook (max 256 chars)
|
|
1954
|
+
* @param {object} [params.setHeader] Custom SIP headers to add to the INVITE, as {name: value}.
|
|
1898
1955
|
* @return {Session} object for call
|
|
1899
1956
|
* @throws NotReady - If phone is not ready to call
|
|
1900
1957
|
* @throws NotAllowed - If there are calls currently connecting
|
|
1958
|
+
* @throws InvalidParameter - If setHeader contains an invalid header name or container
|
|
1901
1959
|
*
|
|
1902
1960
|
*/
|
|
1903
1961
|
call(number, params = {}) {
|
|
1904
1962
|
// Check phone is started first
|
|
1905
1963
|
if (!this._isStarted) {
|
|
1906
|
-
throw new NotReady(
|
|
1964
|
+
throw new NotReady('Phone is not started');
|
|
1907
1965
|
}
|
|
1908
1966
|
|
|
1909
1967
|
// Check we dont have any other connecting calls
|
|
1910
1968
|
if (this._checkForConnectingCalls()) {
|
|
1911
|
-
throw NotAllowed(
|
|
1969
|
+
throw new NotAllowed('Unable to make new calls while others are connecting');
|
|
1912
1970
|
}
|
|
1913
1971
|
|
|
1914
1972
|
// Hold other calls if needed
|
|
@@ -1922,15 +1980,16 @@ class Phone extends EventEmitter {
|
|
|
1922
1980
|
explicitCallerId: null,
|
|
1923
1981
|
explicitShortCallerId: null,
|
|
1924
1982
|
metaInfo: null,
|
|
1983
|
+
setHeader: null,
|
|
1925
1984
|
_isListenCall: false,
|
|
1926
1985
|
},
|
|
1927
1986
|
params,
|
|
1928
1987
|
);
|
|
1929
1988
|
|
|
1930
1989
|
// Trim phone number to remove spaces and '()'
|
|
1931
|
-
number = number.trim().replace(/ |\(|\)/g,
|
|
1990
|
+
number = number.trim().replace(/ |\(|\)/g, '');
|
|
1932
1991
|
|
|
1933
|
-
this.log.info(
|
|
1992
|
+
this.log.info('Calling number: ' + number);
|
|
1934
1993
|
|
|
1935
1994
|
let session = new Session(this);
|
|
1936
1995
|
session.newOutboundSession(number, callParams);
|
|
@@ -1946,12 +2005,12 @@ class Phone extends EventEmitter {
|
|
|
1946
2005
|
* @throws NotAllowed - If there are calls currently connecting
|
|
1947
2006
|
*/
|
|
1948
2007
|
listen(listenKey) {
|
|
1949
|
-
const s = listenKey.split(
|
|
2008
|
+
const s = listenKey.split('@');
|
|
1950
2009
|
let callUUID = s[0];
|
|
1951
|
-
let mediaSwitch =
|
|
2010
|
+
let mediaSwitch = 'sip:' + s[1];
|
|
1952
2011
|
|
|
1953
|
-
this.log.debug(
|
|
1954
|
-
return this.call(
|
|
2012
|
+
this.log.debug('Initiating listen to call with uuid ' + callUUID + ' on media switch ' + mediaSwitch);
|
|
2013
|
+
return this.call('listen-' + callUUID, {
|
|
1955
2014
|
mediaSwitchURI: mediaSwitch,
|
|
1956
2015
|
_isListenCall: true,
|
|
1957
2016
|
});
|
|
@@ -2077,7 +2136,7 @@ class Phone extends EventEmitter {
|
|
|
2077
2136
|
|
|
2078
2137
|
// Begin repeating tones
|
|
2079
2138
|
_startRepeatTone(sequences) {
|
|
2080
|
-
this.log.info(
|
|
2139
|
+
this.log.info('Repeating tone sequence started');
|
|
2081
2140
|
this.log.debug(sequences);
|
|
2082
2141
|
this._repeatToneSequence = sequences;
|
|
2083
2142
|
this._repeatToneSequencePos = 0;
|
|
@@ -2107,7 +2166,7 @@ class Phone extends EventEmitter {
|
|
|
2107
2166
|
// End repeating tones
|
|
2108
2167
|
_endRepeatTone() {
|
|
2109
2168
|
if (this._repeatToneActive) {
|
|
2110
|
-
this.log.info(
|
|
2169
|
+
this.log.info('Repeating tone stopped');
|
|
2111
2170
|
this._repeatToneActive = false;
|
|
2112
2171
|
this.stopTone();
|
|
2113
2172
|
}
|
|
@@ -2118,7 +2177,7 @@ class Phone extends EventEmitter {
|
|
|
2118
2177
|
// Create audio context
|
|
2119
2178
|
const AudioContext = window.AudioContext || window.webkitAudioContext || false;
|
|
2120
2179
|
if (!AudioContext) {
|
|
2121
|
-
throw new NotAllowed(
|
|
2180
|
+
throw new NotAllowed('Audio API not supported by this browser');
|
|
2122
2181
|
}
|
|
2123
2182
|
|
|
2124
2183
|
const ctx = new AudioContext();
|
|
@@ -2146,17 +2205,17 @@ class Phone extends EventEmitter {
|
|
|
2146
2205
|
* @throws NotAllowed If there are connection calls
|
|
2147
2206
|
*/
|
|
2148
2207
|
holdAllCalls() {
|
|
2149
|
-
this.log.info(
|
|
2208
|
+
this.log.info('Holding all active sessions');
|
|
2150
2209
|
// Check all session can be set on hold
|
|
2151
2210
|
if (this._checkForConnectingCalls()) {
|
|
2152
|
-
throw NotAllowed(
|
|
2211
|
+
throw new NotAllowed('A session is currently connecting');
|
|
2153
2212
|
}
|
|
2154
2213
|
|
|
2155
2214
|
// Iterate through all active calls and hold them
|
|
2156
2215
|
for (let key in this._activeSessions) {
|
|
2157
2216
|
if (this._activeSessions.hasOwnProperty(key)) {
|
|
2158
2217
|
let session = this._activeSessions[key];
|
|
2159
|
-
if (session.state ===
|
|
2218
|
+
if (session.state === 'ACTIVE') {
|
|
2160
2219
|
session.hold();
|
|
2161
2220
|
}
|
|
2162
2221
|
}
|
|
@@ -2185,9 +2244,9 @@ class Phone extends EventEmitter {
|
|
|
2185
2244
|
|
|
2186
2245
|
// Fire event
|
|
2187
2246
|
try {
|
|
2188
|
-
this.emit(
|
|
2247
|
+
this.emit('sessionCreated', { session: session });
|
|
2189
2248
|
} catch (e) {
|
|
2190
|
-
this.log.error(
|
|
2249
|
+
this.log.error('Exception in sessionCreated event: ' + e);
|
|
2191
2250
|
}
|
|
2192
2251
|
}
|
|
2193
2252
|
|
|
@@ -2203,9 +2262,9 @@ class Phone extends EventEmitter {
|
|
|
2203
2262
|
|
|
2204
2263
|
// Fire event
|
|
2205
2264
|
try {
|
|
2206
|
-
this.emit(
|
|
2265
|
+
this.emit('sessionRemoved', { session: session });
|
|
2207
2266
|
} catch (e) {
|
|
2208
|
-
this.log.error(
|
|
2267
|
+
this.log.error('Exception in sessionRemoved event: ' + e);
|
|
2209
2268
|
}
|
|
2210
2269
|
|
|
2211
2270
|
delete this._activeSessions[session.__containerId];
|
|
@@ -2230,7 +2289,12 @@ class Phone extends EventEmitter {
|
|
|
2230
2289
|
for (let key in this._activeSessions) {
|
|
2231
2290
|
if (this._activeSessions.hasOwnProperty(key)) {
|
|
2232
2291
|
let session = this._activeSessions[key];
|
|
2233
|
-
if (
|
|
2292
|
+
if (
|
|
2293
|
+
session.state === 'INITIALIZING' ||
|
|
2294
|
+
session.state === 'CALLING' ||
|
|
2295
|
+
session.state === 'PROGRESS' ||
|
|
2296
|
+
session.state === 'INCOMING'
|
|
2297
|
+
) {
|
|
2234
2298
|
return true;
|
|
2235
2299
|
}
|
|
2236
2300
|
}
|
|
@@ -2255,12 +2319,12 @@ class Phone extends EventEmitter {
|
|
|
2255
2319
|
* @param {CONNECTION_STATE} state - The current connection state
|
|
2256
2320
|
*/
|
|
2257
2321
|
|
|
2258
|
-
this.log.info(
|
|
2322
|
+
this.log.info('Connection state: ' + state);
|
|
2259
2323
|
this._connectionState = state;
|
|
2260
2324
|
try {
|
|
2261
|
-
this.emit(
|
|
2325
|
+
this.emit('connectionStateChange', { state: state });
|
|
2262
2326
|
} catch (e) {
|
|
2263
|
-
this.log.error(
|
|
2327
|
+
this.log.error('Exception in connectionStateChange event: ' + e);
|
|
2264
2328
|
}
|
|
2265
2329
|
}
|
|
2266
2330
|
|
|
@@ -2271,9 +2335,9 @@ class Phone extends EventEmitter {
|
|
|
2271
2335
|
*/
|
|
2272
2336
|
_fireIncomingCallEvent(session) {
|
|
2273
2337
|
try {
|
|
2274
|
-
this.emit(
|
|
2338
|
+
this.emit('incomingCall', session);
|
|
2275
2339
|
} catch (e) {
|
|
2276
|
-
this.log.error(
|
|
2340
|
+
this.log.error('Exception in incomingCall event: ' + e);
|
|
2277
2341
|
}
|
|
2278
2342
|
}
|
|
2279
2343
|
|
|
@@ -2326,12 +2390,12 @@ class Phone extends EventEmitter {
|
|
|
2326
2390
|
|
|
2327
2391
|
// Add some extra convenience fields
|
|
2328
2392
|
e.durationStr = createDurationString(e.duration);
|
|
2329
|
-
e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) :
|
|
2330
|
-
e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) :
|
|
2393
|
+
e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) : '';
|
|
2394
|
+
e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) : '';
|
|
2331
2395
|
try {
|
|
2332
|
-
this.emit(
|
|
2396
|
+
this.emit('callDataInfo', e);
|
|
2333
2397
|
} catch (e) {
|
|
2334
|
-
this.log.error(
|
|
2398
|
+
this.log.error('Exception in callDataInfo event: ' + e);
|
|
2335
2399
|
}
|
|
2336
2400
|
}
|
|
2337
2401
|
|
|
@@ -2412,33 +2476,37 @@ class Session extends EventEmitter {
|
|
|
2412
2476
|
let options = {
|
|
2413
2477
|
mediaConstraints: { audio: true, video: false },
|
|
2414
2478
|
pcConfig: {
|
|
2415
|
-
iceServers: [{ urls: [
|
|
2479
|
+
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
2416
2480
|
},
|
|
2417
|
-
extraHeaders: [
|
|
2481
|
+
extraHeaders: [
|
|
2482
|
+
'X-Call-token: ' + this._phone.options.callToken,
|
|
2483
|
+
'X-P-UUID: ' + this._callUUID,
|
|
2484
|
+
'X-Max-cost: ' + callParams.maximumCallCost,
|
|
2485
|
+
],
|
|
2418
2486
|
};
|
|
2419
2487
|
|
|
2420
2488
|
// Append explicit caller id if needed
|
|
2421
2489
|
if (callParams.explicitCallerId) {
|
|
2422
|
-
options.extraHeaders.push(
|
|
2490
|
+
options.extraHeaders.push('X-CID-Number-Id: ' + callParams.explicitCallerId);
|
|
2423
2491
|
}
|
|
2424
2492
|
|
|
2425
2493
|
// Append explicit short cid if needed
|
|
2426
2494
|
if (callParams.explicitShortCallerId) {
|
|
2427
|
-
options.extraHeaders.push(
|
|
2495
|
+
options.extraHeaders.push('X-Short-CID-Id: ' + callParams.explicitShortCallerId);
|
|
2428
2496
|
}
|
|
2429
2497
|
|
|
2430
2498
|
// Append meta info
|
|
2431
2499
|
if (callParams.metaInfo) {
|
|
2432
|
-
options.extraHeaders.push(
|
|
2500
|
+
options.extraHeaders.push('X-Meta-Info: ' + encodeURI(callParams.metaInfo.toString().substr(0, 255)));
|
|
2433
2501
|
}
|
|
2434
2502
|
|
|
2435
2503
|
// Enable recording?
|
|
2436
2504
|
if (callParams.record) {
|
|
2437
2505
|
if (isNaN(callParams.record)) {
|
|
2438
|
-
throw new InvalidParameter(
|
|
2506
|
+
throw new InvalidParameter('Record parameter must be a number');
|
|
2439
2507
|
}
|
|
2440
2508
|
this.log.debug(`Recording enabled and kept for ${callParams.record} days`);
|
|
2441
|
-
options.extraHeaders.push(
|
|
2509
|
+
options.extraHeaders.push('X-Record: ' + callParams.record);
|
|
2442
2510
|
this._isRecording = true;
|
|
2443
2511
|
} else {
|
|
2444
2512
|
this._isRecording = false;
|
|
@@ -2449,8 +2517,8 @@ class Session extends EventEmitter {
|
|
|
2449
2517
|
|
|
2450
2518
|
// Manually specified media switch
|
|
2451
2519
|
if (callParams.mediaSwitchURI) {
|
|
2452
|
-
this.log.debug(
|
|
2453
|
-
options.extraHeaders.push(
|
|
2520
|
+
this.log.debug('Requesting specific media switch ' + callParams.mediaSwitchURI);
|
|
2521
|
+
options.extraHeaders.push('X-RMS: ' + callParams.mediaSwitchURI);
|
|
2454
2522
|
} else {
|
|
2455
2523
|
// Request specific media switch to group calls on same server
|
|
2456
2524
|
const otherSessions = this.getOtherSessions();
|
|
@@ -2458,24 +2526,41 @@ class Session extends EventEmitter {
|
|
|
2458
2526
|
if (otherSessions.length > 0) {
|
|
2459
2527
|
if (otherSessions[0]._mediaSwitch) {
|
|
2460
2528
|
// There are existing active calls, append first media switch address found and request call grouping to this
|
|
2461
|
-
this.log.debug(
|
|
2462
|
-
options.extraHeaders.push(
|
|
2529
|
+
this.log.debug('Requesting call grouping on media switch ' + otherSessions[0]._mediaSwitch);
|
|
2530
|
+
options.extraHeaders.push('X-RMS: sip:' + otherSessions[0]._mediaSwitch);
|
|
2463
2531
|
} else {
|
|
2464
|
-
this.log.warning(
|
|
2532
|
+
this.log.warning('Other sessions exist but media switch address is invalid');
|
|
2465
2533
|
}
|
|
2466
2534
|
} else {
|
|
2467
|
-
this.log.debug(
|
|
2535
|
+
this.log.debug('No other active calls, no media switch grouping requested');
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Custom headers last, so they cannot displace the built-in ones.
|
|
2540
|
+
// Must be before _UA.call() below: jssip clones extraHeaders synchronously
|
|
2541
|
+
// inside connect(), so anything pushed after that call never reaches the INVITE.
|
|
2542
|
+
let customHeaders;
|
|
2543
|
+
try {
|
|
2544
|
+
customHeaders = buildCustomHeaders(callParams.setHeader);
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
if (error instanceof TypeError) {
|
|
2547
|
+
throw new InvalidParameter(error.message);
|
|
2468
2548
|
}
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2551
|
+
for (const header of customHeaders) {
|
|
2552
|
+
this.log.debug('Adding custom header: ' + header);
|
|
2553
|
+
options.extraHeaders.push(header);
|
|
2469
2554
|
}
|
|
2470
2555
|
|
|
2471
2556
|
// Prepare invite uri
|
|
2472
|
-
const uri =
|
|
2557
|
+
const uri = 'sip:' + number + '@telegenta.com';
|
|
2473
2558
|
|
|
2474
2559
|
// Not start call and create sip session
|
|
2475
2560
|
try {
|
|
2476
2561
|
this._initWebRTCSession(this._phone._UA.call(uri, options));
|
|
2477
2562
|
} catch (error) {
|
|
2478
|
-
this.log.error(
|
|
2563
|
+
this.log.error('Error starting call:', error);
|
|
2479
2564
|
return null;
|
|
2480
2565
|
}
|
|
2481
2566
|
|
|
@@ -2486,16 +2571,16 @@ class Session extends EventEmitter {
|
|
|
2486
2571
|
|
|
2487
2572
|
// Create new inbound session
|
|
2488
2573
|
newInboundSession(newSessionRequest) {
|
|
2489
|
-
this._callUUID = newSessionRequest.request.getHeader(
|
|
2490
|
-
this._destinationNumber = newSessionRequest.request.getHeader(
|
|
2491
|
-
this._localNumberName = unescape(newSessionRequest.request.getHeader(
|
|
2574
|
+
this._callUUID = newSessionRequest.request.getHeader('X-P-UUID', 0);
|
|
2575
|
+
this._destinationNumber = newSessionRequest.request.getHeader('X-DID', 0);
|
|
2576
|
+
this._localNumberName = unescape(newSessionRequest.request.getHeader('X-DID-NAME', 0));
|
|
2492
2577
|
this._originName = newSessionRequest.request.from.display_name;
|
|
2493
2578
|
this._originatingNumber = newSessionRequest.request.from.uri.user;
|
|
2494
2579
|
this.log.info(`Incoming call from ${this._originatingNumber} [${this._callUUID}]`);
|
|
2495
2580
|
this.log.debug(newSessionRequest);
|
|
2496
2581
|
|
|
2497
2582
|
// Get media switch info
|
|
2498
|
-
this._mediaSwitch = newSessionRequest.request.getHeader(
|
|
2583
|
+
this._mediaSwitch = newSessionRequest.request.getHeader('X-MS');
|
|
2499
2584
|
this.log.debug(`Media switch handling call: ${this._mediaSwitch}`);
|
|
2500
2585
|
|
|
2501
2586
|
this._initWebRTCSession(newSessionRequest.session);
|
|
@@ -2529,10 +2614,10 @@ class Session extends EventEmitter {
|
|
|
2529
2614
|
const self = this;
|
|
2530
2615
|
|
|
2531
2616
|
// ICE CANDIDATES
|
|
2532
|
-
this._webRTCSession.on(
|
|
2617
|
+
this._webRTCSession.on('icecandidate', function (event) {
|
|
2533
2618
|
const c = event.candidate;
|
|
2534
2619
|
self.log.debug(`Got ICE candidate: ${c.candidate}`);
|
|
2535
|
-
if (c.type ===
|
|
2620
|
+
if (c.type === 'srflx' && c.relatedAddress !== null && c.relatedPort !== null) {
|
|
2536
2621
|
self.log.info(`Accepting ICE candidate: ${c.candidate}`);
|
|
2537
2622
|
|
|
2538
2623
|
// Clear timer first
|
|
@@ -2555,24 +2640,24 @@ class Session extends EventEmitter {
|
|
|
2555
2640
|
}
|
|
2556
2641
|
});
|
|
2557
2642
|
|
|
2558
|
-
this._webRTCSession.on(
|
|
2643
|
+
this._webRTCSession.on('connecting', function (/* data */) {
|
|
2559
2644
|
self._startDurationUpdateTimer();
|
|
2560
2645
|
self._startRTCPStatsUpdateTimer();
|
|
2561
|
-
self._setSessionState(
|
|
2646
|
+
self._setSessionState('INITIALIZING');
|
|
2562
2647
|
});
|
|
2563
2648
|
|
|
2564
|
-
this._webRTCSession.on(
|
|
2649
|
+
this._webRTCSession.on('sending', function (/* data */) {
|
|
2565
2650
|
// Play call initialized tone if needed
|
|
2566
2651
|
if (self._phone.options.playCallInitializedTone) {
|
|
2567
2652
|
self._phone._startRepeatTone(ST_CALLINIT);
|
|
2568
2653
|
}
|
|
2569
2654
|
|
|
2570
|
-
self._setSessionState(
|
|
2655
|
+
self._setSessionState('CALLING');
|
|
2571
2656
|
});
|
|
2572
2657
|
|
|
2573
|
-
this._webRTCSession.on(
|
|
2574
|
-
if (data.originator ===
|
|
2575
|
-
self._setSessionState(
|
|
2658
|
+
this._webRTCSession.on('progress', function (data) {
|
|
2659
|
+
if (data.originator === 'remote') {
|
|
2660
|
+
self._setSessionState('PROGRESS', { code: data.response.status_code });
|
|
2576
2661
|
self._phone._endRepeatTone();
|
|
2577
2662
|
|
|
2578
2663
|
// Ringback handling on 180 RINGING
|
|
@@ -2587,14 +2672,14 @@ class Session extends EventEmitter {
|
|
|
2587
2672
|
try {
|
|
2588
2673
|
self.hangup();
|
|
2589
2674
|
} catch (e) {
|
|
2590
|
-
self.log.info(
|
|
2675
|
+
self.log.info('No active session, ignoring answer call timeout');
|
|
2591
2676
|
}
|
|
2592
2677
|
}, self._answerCallTimeout * 1000);
|
|
2593
2678
|
}
|
|
2594
2679
|
}
|
|
2595
2680
|
});
|
|
2596
2681
|
|
|
2597
|
-
this._webRTCSession.on(
|
|
2682
|
+
this._webRTCSession.on('accepted', function (data) {
|
|
2598
2683
|
// Always end ringsback
|
|
2599
2684
|
self._phone._endRepeatTone();
|
|
2600
2685
|
|
|
@@ -2607,27 +2692,27 @@ class Session extends EventEmitter {
|
|
|
2607
2692
|
}
|
|
2608
2693
|
|
|
2609
2694
|
// Get initial recording state
|
|
2610
|
-
if (data.originator ===
|
|
2611
|
-
const recState = data.response.getHeader(
|
|
2612
|
-
if (recState ===
|
|
2613
|
-
self.log.debug(
|
|
2695
|
+
if (data.originator === 'remote') {
|
|
2696
|
+
const recState = data.response.getHeader('X-Recording');
|
|
2697
|
+
if (recState === 'true') {
|
|
2698
|
+
self.log.debug('Call is being prerecorded');
|
|
2614
2699
|
// self._isRecording = true;
|
|
2615
2700
|
// self.emit('recordingStateChange', {state: true, error: null});
|
|
2616
2701
|
} else {
|
|
2617
|
-
self.log.debug(
|
|
2702
|
+
self.log.debug('No call recording started yet');
|
|
2618
2703
|
}
|
|
2619
2704
|
|
|
2620
2705
|
// Get media switch info
|
|
2621
|
-
self._mediaSwitch = data.response.getHeader(
|
|
2706
|
+
self._mediaSwitch = data.response.getHeader('X-MS');
|
|
2622
2707
|
self.log.debug(`Media switch handling call: ${self._mediaSwitch}`);
|
|
2623
2708
|
}
|
|
2624
2709
|
});
|
|
2625
2710
|
|
|
2626
|
-
this._webRTCSession.on(
|
|
2627
|
-
self._setSessionState(
|
|
2711
|
+
this._webRTCSession.on('confirmed', function (/* data */) {
|
|
2712
|
+
self._setSessionState('ACTIVE');
|
|
2628
2713
|
});
|
|
2629
2714
|
|
|
2630
|
-
this._webRTCSession.on(
|
|
2715
|
+
this._webRTCSession.on('ended', function (/* data */) {
|
|
2631
2716
|
// End any ringsbacks if needed
|
|
2632
2717
|
self._phone._endRepeatTone();
|
|
2633
2718
|
|
|
@@ -2638,10 +2723,10 @@ class Session extends EventEmitter {
|
|
|
2638
2723
|
clearTimeout(self._answerCallTimer);
|
|
2639
2724
|
}
|
|
2640
2725
|
|
|
2641
|
-
self._setSessionState(
|
|
2726
|
+
self._setSessionState('TERMINATED');
|
|
2642
2727
|
});
|
|
2643
2728
|
|
|
2644
|
-
this._webRTCSession.on(
|
|
2729
|
+
this._webRTCSession.on('failed', function (data) {
|
|
2645
2730
|
// End any ringsbacks if needed
|
|
2646
2731
|
self._phone._endRepeatTone();
|
|
2647
2732
|
|
|
@@ -2659,54 +2744,54 @@ class Session extends EventEmitter {
|
|
|
2659
2744
|
if (data.message) {
|
|
2660
2745
|
m = `${data.message.status_code} ${data.message.reason_phrase}`;
|
|
2661
2746
|
} else {
|
|
2662
|
-
m =
|
|
2747
|
+
m = '';
|
|
2663
2748
|
}
|
|
2664
2749
|
|
|
2665
|
-
self._setSessionState(
|
|
2750
|
+
self._setSessionState('FAILED', { cause: data.cause, message: m });
|
|
2666
2751
|
});
|
|
2667
2752
|
|
|
2668
|
-
this._webRTCSession.on(
|
|
2669
|
-
self.log.debug(
|
|
2753
|
+
this._webRTCSession.on('dtmf', function (data) {
|
|
2754
|
+
self.log.debug('Session DTMF', data);
|
|
2670
2755
|
});
|
|
2671
2756
|
|
|
2672
|
-
this._webRTCSession.on(
|
|
2673
|
-
self.log.debug(
|
|
2757
|
+
this._webRTCSession.on('newInfo', function (data) {
|
|
2758
|
+
self.log.debug('New session info', data);
|
|
2674
2759
|
});
|
|
2675
2760
|
|
|
2676
|
-
this._webRTCSession.on(
|
|
2761
|
+
this._webRTCSession.on('hold', function (/* data */) {
|
|
2677
2762
|
self._isOnHold = true;
|
|
2678
2763
|
self._fireHoldStateEvent();
|
|
2679
|
-
self._setSessionState(
|
|
2764
|
+
self._setSessionState('ON_HOLD');
|
|
2680
2765
|
});
|
|
2681
2766
|
|
|
2682
|
-
this._webRTCSession.on(
|
|
2767
|
+
this._webRTCSession.on('unhold', function (/* data */) {
|
|
2683
2768
|
self._isOnHold = false;
|
|
2684
2769
|
self._fireHoldStateEvent();
|
|
2685
|
-
self._setSessionState(
|
|
2770
|
+
self._setSessionState('ACTIVE');
|
|
2686
2771
|
});
|
|
2687
2772
|
|
|
2688
|
-
this._webRTCSession.on(
|
|
2689
|
-
self.log.debug(
|
|
2773
|
+
this._webRTCSession.on('muted', function (data) {
|
|
2774
|
+
self.log.debug('Microphone muted', data);
|
|
2690
2775
|
self._isMuted = true;
|
|
2691
2776
|
self._fireMuteStateEvent();
|
|
2692
2777
|
});
|
|
2693
2778
|
|
|
2694
|
-
this._webRTCSession.on(
|
|
2695
|
-
self.log.debug(
|
|
2779
|
+
this._webRTCSession.on('unmuted', function (data) {
|
|
2780
|
+
self.log.debug('Microphone unmuted', data);
|
|
2696
2781
|
self._isMuted = false;
|
|
2697
2782
|
self._fireMuteStateEvent();
|
|
2698
2783
|
});
|
|
2699
2784
|
|
|
2700
|
-
this._webRTCSession.on(
|
|
2701
|
-
self.log.debug(
|
|
2785
|
+
this._webRTCSession.on('reinvite', function (data) {
|
|
2786
|
+
self.log.debug('Session reinvite', data);
|
|
2702
2787
|
});
|
|
2703
2788
|
|
|
2704
|
-
this._webRTCSession.on(
|
|
2705
|
-
self.log.debug(
|
|
2789
|
+
this._webRTCSession.on('update', function (data) {
|
|
2790
|
+
self.log.debug('Session update', data);
|
|
2706
2791
|
});
|
|
2707
2792
|
|
|
2708
|
-
this._webRTCSession.on(
|
|
2709
|
-
self.log.error(
|
|
2793
|
+
this._webRTCSession.on('getusermediafailed', function (data) {
|
|
2794
|
+
self.log.error('Getusermedia failed', data);
|
|
2710
2795
|
});
|
|
2711
2796
|
|
|
2712
2797
|
// If connection connect audio stream
|
|
@@ -2715,7 +2800,7 @@ class Session extends EventEmitter {
|
|
|
2715
2800
|
}
|
|
2716
2801
|
|
|
2717
2802
|
// DEBUG STUFF
|
|
2718
|
-
this.log.debug(
|
|
2803
|
+
this.log.debug('WebRTCSession object:');
|
|
2719
2804
|
this.log.debug(this);
|
|
2720
2805
|
}
|
|
2721
2806
|
|
|
@@ -2726,13 +2811,13 @@ class Session extends EventEmitter {
|
|
|
2726
2811
|
*/
|
|
2727
2812
|
hangup(disposition = null) {
|
|
2728
2813
|
if (!disposition) {
|
|
2729
|
-
this.log.info(
|
|
2814
|
+
this.log.info('Hanging up call');
|
|
2730
2815
|
this._webRTCSession.terminate();
|
|
2731
2816
|
} else {
|
|
2732
2817
|
this.log.info(`Hanging up call with disposition ${disposition}`);
|
|
2733
2818
|
this._manualDisposition = disposition;
|
|
2734
2819
|
this._webRTCSession.terminate({
|
|
2735
|
-
extraHeaders: [
|
|
2820
|
+
extraHeaders: ['X-HDISP: ' + disposition],
|
|
2736
2821
|
});
|
|
2737
2822
|
}
|
|
2738
2823
|
}
|
|
@@ -2745,14 +2830,14 @@ class Session extends EventEmitter {
|
|
|
2745
2830
|
let options = {
|
|
2746
2831
|
mediaConstraints: { audio: true, video: false },
|
|
2747
2832
|
pcConfig: {
|
|
2748
|
-
iceServers: [{ urls: [
|
|
2833
|
+
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
2749
2834
|
},
|
|
2750
|
-
extraHeaders: [
|
|
2835
|
+
extraHeaders: ['X-Call-token: ' + this._phone.options.callToken],
|
|
2751
2836
|
};
|
|
2752
2837
|
|
|
2753
2838
|
// Append meta info if needed
|
|
2754
2839
|
if (metaInfo) {
|
|
2755
|
-
options.extraHeaders.push(
|
|
2840
|
+
options.extraHeaders.push('X-Meta-Info: ' + encodeURI(metaInfo.toString().substr(0, 255)));
|
|
2756
2841
|
}
|
|
2757
2842
|
|
|
2758
2843
|
this._webRTCSession.answer(options);
|
|
@@ -2766,8 +2851,8 @@ class Session extends EventEmitter {
|
|
|
2766
2851
|
* @param statusCode - SIP Status code
|
|
2767
2852
|
* @param reasonPhrase - Reason phrase
|
|
2768
2853
|
*/
|
|
2769
|
-
reject(statusCode = 603, reasonPhrase =
|
|
2770
|
-
this.log.info(
|
|
2854
|
+
reject(statusCode = 603, reasonPhrase = 'Rejected call') {
|
|
2855
|
+
this.log.info('Rejecting call');
|
|
2771
2856
|
this._webRTCSession.terminate({
|
|
2772
2857
|
status_code: statusCode,
|
|
2773
2858
|
reason_phrase: reasonPhrase,
|
|
@@ -2808,7 +2893,7 @@ class Session extends EventEmitter {
|
|
|
2808
2893
|
|
|
2809
2894
|
// Set destination number
|
|
2810
2895
|
set destinationNumber(number) {
|
|
2811
|
-
this.log.debug(
|
|
2896
|
+
this.log.debug('Setting destination number to: ' + number);
|
|
2812
2897
|
this._destinationNumber = number;
|
|
2813
2898
|
}
|
|
2814
2899
|
|
|
@@ -2826,53 +2911,77 @@ class Session extends EventEmitter {
|
|
|
2826
2911
|
*/
|
|
2827
2912
|
get direction() {
|
|
2828
2913
|
if (this._isListenCall) {
|
|
2829
|
-
this.log.debug(
|
|
2830
|
-
return
|
|
2831
|
-
} else if (this._webRTCSession.direction ===
|
|
2832
|
-
this.log.debug(
|
|
2833
|
-
return
|
|
2834
|
-
} else if (this._webRTCSession.direction ===
|
|
2835
|
-
this.log.debug(
|
|
2836
|
-
return
|
|
2914
|
+
this.log.debug('Call direction is LISTEN');
|
|
2915
|
+
return 'LISTEN';
|
|
2916
|
+
} else if (this._webRTCSession.direction === 'incoming') {
|
|
2917
|
+
this.log.debug('Call direction is INBOUND');
|
|
2918
|
+
return 'INBOUND';
|
|
2919
|
+
} else if (this._webRTCSession.direction === 'outgoing') {
|
|
2920
|
+
this.log.debug('Call direction is OUTBOUND');
|
|
2921
|
+
return 'OUTBOUND';
|
|
2837
2922
|
} else {
|
|
2838
|
-
this.log.error(
|
|
2839
|
-
return
|
|
2923
|
+
this.log.error('Unknown call direction: ' + this._webRTCSession.direction);
|
|
2924
|
+
return 'UNKNOWN';
|
|
2840
2925
|
}
|
|
2841
2926
|
}
|
|
2842
2927
|
|
|
2928
|
+
/**
|
|
2929
|
+
* Raw WebRTC statistics for this session's peer connection.
|
|
2930
|
+
*
|
|
2931
|
+
* Returns the browser's own `RTCStatsReport` untouched, so callers can read
|
|
2932
|
+
* any statistic the browser exposes - RTP packet and byte counters, the
|
|
2933
|
+
* DTLS/ICE transport state, codec and candidate-pair details.
|
|
2934
|
+
*
|
|
2935
|
+
* @returns {Promise<RTCStatsReport>} The peer connection's statistics
|
|
2936
|
+
* @throws NoActiveSession - If the session has no established peer connection
|
|
2937
|
+
* (thrown synchronously, before the promise is created)
|
|
2938
|
+
*
|
|
2939
|
+
* @example
|
|
2940
|
+
* const report = await session.getRTCStats();
|
|
2941
|
+
* for (const stat of report.values()) {
|
|
2942
|
+
* if (stat.type === 'inbound-rtp') console.log(stat.packetsReceived);
|
|
2943
|
+
* }
|
|
2944
|
+
*/
|
|
2945
|
+
getRTCStats() {
|
|
2946
|
+
if (!this._webRTCSession || !this._webRTCSession.connection) {
|
|
2947
|
+
throw new NoActiveSession('no peer connection for this session');
|
|
2948
|
+
}
|
|
2949
|
+
return this._webRTCSession.connection.getStats();
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2843
2952
|
// RECORDING
|
|
2844
2953
|
/**
|
|
2845
2954
|
* Start recording
|
|
2846
2955
|
*/
|
|
2847
2956
|
startRecording() {
|
|
2848
2957
|
const opt = {
|
|
2849
|
-
extraHeaders: [
|
|
2958
|
+
extraHeaders: ['Record: on'],
|
|
2850
2959
|
};
|
|
2851
2960
|
|
|
2852
|
-
this.log.debug(
|
|
2961
|
+
this.log.debug('Request start recording');
|
|
2853
2962
|
|
|
2854
2963
|
const self = this;
|
|
2855
|
-
this._webRTCSession.once(
|
|
2856
|
-
if (data.originator ===
|
|
2857
|
-
data.info.on(
|
|
2858
|
-
self.log.info(
|
|
2964
|
+
this._webRTCSession.once('newInfo', (data) => {
|
|
2965
|
+
if (data.originator === 'local') {
|
|
2966
|
+
data.info.on('succeeded', function (/* data */) {
|
|
2967
|
+
self.log.info('Recording started successfully');
|
|
2859
2968
|
self._isRecording = true;
|
|
2860
2969
|
try {
|
|
2861
|
-
self.emit(
|
|
2970
|
+
self.emit('recordingStateChange', { state: true, error: null });
|
|
2862
2971
|
} catch (e) {
|
|
2863
|
-
self.log.error(
|
|
2972
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
2864
2973
|
}
|
|
2865
2974
|
});
|
|
2866
2975
|
|
|
2867
|
-
data.info.on(
|
|
2976
|
+
data.info.on('failed', function (data) {
|
|
2868
2977
|
self._recordingError = data.response.reason_phrase;
|
|
2869
|
-
self.log.error(
|
|
2978
|
+
self.log.error('Recording start failed: ' + self._recordingError);
|
|
2870
2979
|
self._fireRecordingStateEvent();
|
|
2871
2980
|
});
|
|
2872
2981
|
}
|
|
2873
2982
|
});
|
|
2874
2983
|
|
|
2875
|
-
this._webRTCSession.sendInfo(
|
|
2984
|
+
this._webRTCSession.sendInfo('application/info', null, opt);
|
|
2876
2985
|
}
|
|
2877
2986
|
|
|
2878
2987
|
/**
|
|
@@ -2880,35 +2989,35 @@ class Session extends EventEmitter {
|
|
|
2880
2989
|
*/
|
|
2881
2990
|
stopRecording() {
|
|
2882
2991
|
const opt = {
|
|
2883
|
-
extraHeaders: [
|
|
2992
|
+
extraHeaders: ['Record: off'],
|
|
2884
2993
|
};
|
|
2885
2994
|
|
|
2886
|
-
this.log.debug(
|
|
2995
|
+
this.log.debug('Request stop recording');
|
|
2887
2996
|
|
|
2888
2997
|
const self = this;
|
|
2889
2998
|
|
|
2890
|
-
this._webRTCSession.once(
|
|
2891
|
-
data.info.on(
|
|
2892
|
-
self.log.info(
|
|
2999
|
+
this._webRTCSession.once('newInfo', (data) => {
|
|
3000
|
+
data.info.on('succeeded', function (/* data */) {
|
|
3001
|
+
self.log.info('Recording stopped successfully');
|
|
2893
3002
|
self._isRecording = false;
|
|
2894
3003
|
try {
|
|
2895
|
-
self.emit(
|
|
3004
|
+
self.emit('recordingStateChange', { state: false, error: null });
|
|
2896
3005
|
} catch (e) {
|
|
2897
|
-
self.log.error(
|
|
3006
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
2898
3007
|
}
|
|
2899
3008
|
});
|
|
2900
3009
|
|
|
2901
|
-
data.info.on(
|
|
2902
|
-
self.log.error(
|
|
3010
|
+
data.info.on('failed', function (data) {
|
|
3011
|
+
self.log.error('Recording stopped failed: ' + data.response.reason_phrase);
|
|
2903
3012
|
try {
|
|
2904
|
-
self.emit(
|
|
3013
|
+
self.emit('recordingStateChange', { state: self._isRecording, error: data.response.reason_phrase });
|
|
2905
3014
|
} catch (e) {
|
|
2906
|
-
self.log.error(
|
|
3015
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
2907
3016
|
}
|
|
2908
3017
|
});
|
|
2909
3018
|
});
|
|
2910
3019
|
|
|
2911
|
-
this._webRTCSession.sendInfo(
|
|
3020
|
+
this._webRTCSession.sendInfo('application/info', null, opt);
|
|
2912
3021
|
}
|
|
2913
3022
|
|
|
2914
3023
|
/**
|
|
@@ -2937,7 +3046,7 @@ class Session extends EventEmitter {
|
|
|
2937
3046
|
* Mute microphone
|
|
2938
3047
|
*/
|
|
2939
3048
|
muteMic() {
|
|
2940
|
-
this.log.info(
|
|
3049
|
+
this.log.info('Mute microphone');
|
|
2941
3050
|
this._webRTCSession.mute();
|
|
2942
3051
|
}
|
|
2943
3052
|
|
|
@@ -2945,7 +3054,7 @@ class Session extends EventEmitter {
|
|
|
2945
3054
|
* Unmute microphone
|
|
2946
3055
|
*/
|
|
2947
3056
|
unmuteMic() {
|
|
2948
|
-
this.log.info(
|
|
3057
|
+
this.log.info('Unmute microphone');
|
|
2949
3058
|
this._webRTCSession.unmute();
|
|
2950
3059
|
}
|
|
2951
3060
|
|
|
@@ -2973,7 +3082,7 @@ class Session extends EventEmitter {
|
|
|
2973
3082
|
* Hold call
|
|
2974
3083
|
*/
|
|
2975
3084
|
hold() {
|
|
2976
|
-
this.log.info(
|
|
3085
|
+
this.log.info('Hold call');
|
|
2977
3086
|
this._webRTCSession.hold();
|
|
2978
3087
|
}
|
|
2979
3088
|
|
|
@@ -2981,7 +3090,7 @@ class Session extends EventEmitter {
|
|
|
2981
3090
|
* Unhold call
|
|
2982
3091
|
*/
|
|
2983
3092
|
unhold() {
|
|
2984
|
-
this.log.info(
|
|
3093
|
+
this.log.info('Uncall call');
|
|
2985
3094
|
this._phone.holdAllCalls();
|
|
2986
3095
|
this._webRTCSession.unhold();
|
|
2987
3096
|
}
|
|
@@ -3014,10 +3123,10 @@ class Session extends EventEmitter {
|
|
|
3014
3123
|
sendDTMF(code, playLocal = true) {
|
|
3015
3124
|
// Check for correct DTMF and get frequencies
|
|
3016
3125
|
if (!(code in DTMF_FREQUENCY_TABLE)) {
|
|
3017
|
-
throw new InvalidParameter(
|
|
3126
|
+
throw new InvalidParameter('Invalid DTMF code');
|
|
3018
3127
|
}
|
|
3019
3128
|
|
|
3020
|
-
this.log.info(
|
|
3129
|
+
this.log.info('Sending DTMF: ' + code);
|
|
3021
3130
|
this._webRTCSession.sendDTMF(code);
|
|
3022
3131
|
|
|
3023
3132
|
if (playLocal) {
|
|
@@ -3025,15 +3134,15 @@ class Session extends EventEmitter {
|
|
|
3025
3134
|
|
|
3026
3135
|
// First mute microphone before playing DTMF
|
|
3027
3136
|
if (this._isMuted) {
|
|
3028
|
-
this.log.debug(
|
|
3137
|
+
this.log.debug('Mic already muted, just sending DTMF');
|
|
3029
3138
|
this._phone.playTone(500, f[0], f[1]);
|
|
3030
3139
|
} else {
|
|
3031
|
-
this.log.debug(
|
|
3140
|
+
this.log.debug('Mute mic while sending DTMF');
|
|
3032
3141
|
this._webRTCSession.mute();
|
|
3033
3142
|
|
|
3034
3143
|
const self = this;
|
|
3035
3144
|
this._phone.playTone(500, f[0], f[1], function () {
|
|
3036
|
-
self.log.debug(
|
|
3145
|
+
self.log.debug('Unmute mic after DTMF');
|
|
3037
3146
|
self._webRTCSession.unmute();
|
|
3038
3147
|
});
|
|
3039
3148
|
}
|
|
@@ -3049,20 +3158,22 @@ class Session extends EventEmitter {
|
|
|
3049
3158
|
this.log.info(`Transferring ${this.uuid} => ${dstSession.uuid}`);
|
|
3050
3159
|
|
|
3051
3160
|
// Create ReferSubscriber event class
|
|
3052
|
-
const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, {
|
|
3161
|
+
const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, {
|
|
3162
|
+
replaces: dstSession._webRTCSession,
|
|
3163
|
+
});
|
|
3053
3164
|
|
|
3054
3165
|
const self = this;
|
|
3055
3166
|
|
|
3056
3167
|
// Setup some logging
|
|
3057
|
-
referSubscriber.on(
|
|
3058
|
-
self.log.info(
|
|
3168
|
+
referSubscriber.on('requestSucceeded', function () {
|
|
3169
|
+
self.log.info('Transfer completed successfully');
|
|
3059
3170
|
});
|
|
3060
3171
|
|
|
3061
|
-
referSubscriber.on(
|
|
3172
|
+
referSubscriber.on('requestFailed', function (cause) {
|
|
3062
3173
|
self.log.error(`Transfer rejected: ${cause}`);
|
|
3063
3174
|
});
|
|
3064
3175
|
|
|
3065
|
-
referSubscriber.on(
|
|
3176
|
+
referSubscriber.on('failed', function (cause) {
|
|
3066
3177
|
self.log.error(`Transfer failed: ${cause}`);
|
|
3067
3178
|
});
|
|
3068
3179
|
}
|
|
@@ -3080,14 +3191,14 @@ class Session extends EventEmitter {
|
|
|
3080
3191
|
playback(media_file_id, hangup_after = false) {
|
|
3081
3192
|
// Check event service is enabled first
|
|
3082
3193
|
if (!this._phone._options.connectEventService) {
|
|
3083
|
-
throw new NotAllowed(
|
|
3194
|
+
throw new NotAllowed('Event service not enabled');
|
|
3084
3195
|
}
|
|
3085
3196
|
|
|
3086
3197
|
this.log.info(`Playback media file ${media_file_id} hangup after ${hangup_after}`);
|
|
3087
3198
|
|
|
3088
|
-
const p = this._phone._eventClient.fireEvent(
|
|
3199
|
+
const p = this._phone._eventClient.fireEvent('call_command', {
|
|
3089
3200
|
call_uuid: this.uuid,
|
|
3090
|
-
call_command:
|
|
3201
|
+
call_command: 'playback',
|
|
3091
3202
|
media_file_id: media_file_id,
|
|
3092
3203
|
hangup_after: hangup_after,
|
|
3093
3204
|
});
|
|
@@ -3103,7 +3214,7 @@ class Session extends EventEmitter {
|
|
|
3103
3214
|
// When playback starts set state to playback
|
|
3104
3215
|
p.then(() => {
|
|
3105
3216
|
// Set session state
|
|
3106
|
-
this._setSessionState(
|
|
3217
|
+
this._setSessionState('PLAYBACK');
|
|
3107
3218
|
});
|
|
3108
3219
|
}
|
|
3109
3220
|
|
|
@@ -3119,12 +3230,12 @@ class Session extends EventEmitter {
|
|
|
3119
3230
|
* @throws InvalidOptions - If invalid mode given
|
|
3120
3231
|
*/
|
|
3121
3232
|
setWhisperMode(mode = 0) {
|
|
3122
|
-
if (!this.direction ===
|
|
3123
|
-
throw new NotAllowed(
|
|
3233
|
+
if (!this.direction === 'LISTEN') {
|
|
3234
|
+
throw new NotAllowed('Call is not correct type (listen)');
|
|
3124
3235
|
}
|
|
3125
3236
|
|
|
3126
3237
|
if (mode < 0 || mode > 2) {
|
|
3127
|
-
throw new InvalidOptions(
|
|
3238
|
+
throw new InvalidOptions('Invalid mode');
|
|
3128
3239
|
}
|
|
3129
3240
|
|
|
3130
3241
|
let t_dtmf_mode;
|
|
@@ -3182,13 +3293,13 @@ class Session extends EventEmitter {
|
|
|
3182
3293
|
duration: Date.now() - self._callBeginTime,
|
|
3183
3294
|
durationStr: createDurationString(Date.now() - self._callBeginTime),
|
|
3184
3295
|
activeDuration: activeDuration,
|
|
3185
|
-
activeDurationStr: activeDuration ? createDurationString(activeDuration) :
|
|
3296
|
+
activeDurationStr: activeDuration ? createDurationString(activeDuration) : '',
|
|
3186
3297
|
};
|
|
3187
3298
|
|
|
3188
3299
|
try {
|
|
3189
|
-
self.emit(
|
|
3300
|
+
self.emit('callDurationUpdate', e);
|
|
3190
3301
|
} catch (e) {
|
|
3191
|
-
self.log.error(
|
|
3302
|
+
self.log.error('Exception in call duration update event: ' + e);
|
|
3192
3303
|
}
|
|
3193
3304
|
}, 1000);
|
|
3194
3305
|
}
|
|
@@ -3252,40 +3363,46 @@ class Session extends EventEmitter {
|
|
|
3252
3363
|
for (let report of data.values()) {
|
|
3253
3364
|
// this.log.debug(report);
|
|
3254
3365
|
switch (report.type) {
|
|
3255
|
-
case
|
|
3366
|
+
case 'outbound-rtp':
|
|
3256
3367
|
// this.log.debug('Got: outbound-rtp');
|
|
3257
3368
|
// this.log.debug(report);
|
|
3258
3369
|
// Packets
|
|
3259
3370
|
totalReport.outboundPacketsSent = report.packetsSent;
|
|
3260
|
-
deltaReport.outboundPacketsSent =
|
|
3371
|
+
deltaReport.outboundPacketsSent =
|
|
3372
|
+
report.packetsSent - this._currentReport.outboundPacketsSent;
|
|
3261
3373
|
// Bytes
|
|
3262
3374
|
totalReport.outboundBytesSent = report.bytesSent;
|
|
3263
|
-
deltaReport.outboundBytesSent =
|
|
3375
|
+
deltaReport.outboundBytesSent =
|
|
3376
|
+
report.bytesSent - this._currentReport.outboundBytesSent;
|
|
3264
3377
|
break;
|
|
3265
3378
|
|
|
3266
|
-
case
|
|
3379
|
+
case 'inbound-rtp':
|
|
3267
3380
|
// this.log.debug('Got: inbound-rtp');
|
|
3268
3381
|
// this.log.debug(report);
|
|
3269
3382
|
// Packets
|
|
3270
3383
|
totalReport.inboundPacketsReceived = report.packetsReceived;
|
|
3271
|
-
deltaReport.inboundPacketsReceived =
|
|
3384
|
+
deltaReport.inboundPacketsReceived =
|
|
3385
|
+
report.packetsReceived - this._currentReport.inboundPacketsReceived;
|
|
3272
3386
|
// Bytes
|
|
3273
3387
|
totalReport.inboundBytesReceived = report.bytesReceived;
|
|
3274
|
-
deltaReport.inboundBytesReceived =
|
|
3388
|
+
deltaReport.inboundBytesReceived =
|
|
3389
|
+
report.bytesReceived - this._currentReport.inboundBytesReceived;
|
|
3275
3390
|
// Loss
|
|
3276
3391
|
totalReport.inboundPacketsLost = report.packetsLost;
|
|
3277
|
-
deltaReport.inboundPacketsLost =
|
|
3392
|
+
deltaReport.inboundPacketsLost =
|
|
3393
|
+
report.packetsLost - this._currentReport.inboundPacketsLost;
|
|
3278
3394
|
// Jitter
|
|
3279
3395
|
totalReport.inboundJitter = report.jitter * 1000;
|
|
3280
3396
|
deltaReport.inboundJitter = report.jitter * 1000;
|
|
3281
3397
|
break;
|
|
3282
3398
|
|
|
3283
|
-
case
|
|
3399
|
+
case 'remote-inbound-rtp':
|
|
3284
3400
|
// this.log.debug('Got: remote-inbound-rtp');
|
|
3285
3401
|
// this.log.debug(report);
|
|
3286
3402
|
// Loss
|
|
3287
3403
|
totalReport.outboundPacketsLost = report.packetsLost;
|
|
3288
|
-
deltaReport.outboundPacketsLost =
|
|
3404
|
+
deltaReport.outboundPacketsLost =
|
|
3405
|
+
report.packetsLost - this._currentReport.outboundPacketsLost;
|
|
3289
3406
|
// Jitter
|
|
3290
3407
|
totalReport.outboundJitter = report.jitter * 1000;
|
|
3291
3408
|
deltaReport.outboundJitter = report.jitter * 1000;
|
|
@@ -3307,9 +3424,9 @@ class Session extends EventEmitter {
|
|
|
3307
3424
|
};
|
|
3308
3425
|
|
|
3309
3426
|
try {
|
|
3310
|
-
this.emit(
|
|
3427
|
+
this.emit('callQualityReportUpdate', finalReport);
|
|
3311
3428
|
} catch (e) {
|
|
3312
|
-
this.log.error(
|
|
3429
|
+
this.log.error('Exception in call quality report update event: ' + e);
|
|
3313
3430
|
}
|
|
3314
3431
|
}
|
|
3315
3432
|
});
|
|
@@ -3333,27 +3450,28 @@ class Session extends EventEmitter {
|
|
|
3333
3450
|
// this.log.debug(`R factor loss effect ${lossEffect} / latency effect ${latencyEffect}`);
|
|
3334
3451
|
|
|
3335
3452
|
report.rFactor = Math.max(0, 100 - lossEffect - latencyEffect);
|
|
3336
|
-
report.mos =
|
|
3453
|
+
report.mos =
|
|
3454
|
+
1 + 0.035 * report.rFactor + 0.000007 * report.rFactor * (report.rFactor - 60) * (100 - report.rFactor);
|
|
3337
3455
|
|
|
3338
3456
|
// Quality string
|
|
3339
3457
|
switch (true) {
|
|
3340
3458
|
case report.rFactor < 50:
|
|
3341
|
-
report.qualityString =
|
|
3459
|
+
report.qualityString = 'Bad';
|
|
3342
3460
|
break;
|
|
3343
3461
|
case report.rFactor < 70:
|
|
3344
|
-
report.qualityString =
|
|
3462
|
+
report.qualityString = 'Poor';
|
|
3345
3463
|
break;
|
|
3346
3464
|
case report.rFactor < 80:
|
|
3347
|
-
report.qualityString =
|
|
3465
|
+
report.qualityString = 'Fair';
|
|
3348
3466
|
break;
|
|
3349
3467
|
case report.rFactor < 90:
|
|
3350
|
-
report.qualityString =
|
|
3468
|
+
report.qualityString = 'Good';
|
|
3351
3469
|
break;
|
|
3352
3470
|
case report.rFactor <= 100:
|
|
3353
|
-
report.qualityString =
|
|
3471
|
+
report.qualityString = 'Excellent';
|
|
3354
3472
|
break;
|
|
3355
3473
|
default:
|
|
3356
|
-
report.qualityString =
|
|
3474
|
+
report.qualityString = 'Error';
|
|
3357
3475
|
}
|
|
3358
3476
|
|
|
3359
3477
|
return report;
|
|
@@ -3387,7 +3505,7 @@ class Session extends EventEmitter {
|
|
|
3387
3505
|
*
|
|
3388
3506
|
*/
|
|
3389
3507
|
|
|
3390
|
-
this.log.debug(
|
|
3508
|
+
this.log.debug('Session state: ' + state);
|
|
3391
3509
|
if (info) {
|
|
3392
3510
|
this.log.debug(info);
|
|
3393
3511
|
}
|
|
@@ -3395,7 +3513,7 @@ class Session extends EventEmitter {
|
|
|
3395
3513
|
this._sessionState = state;
|
|
3396
3514
|
try {
|
|
3397
3515
|
this.emit(
|
|
3398
|
-
|
|
3516
|
+
'sessionStateChange',
|
|
3399
3517
|
Object.assign(
|
|
3400
3518
|
{
|
|
3401
3519
|
state: state,
|
|
@@ -3405,51 +3523,51 @@ class Session extends EventEmitter {
|
|
|
3405
3523
|
),
|
|
3406
3524
|
);
|
|
3407
3525
|
} catch (e) {
|
|
3408
|
-
this.log.error(
|
|
3526
|
+
this.log.error('Exception in sessionStateChange event:' + e);
|
|
3409
3527
|
}
|
|
3410
3528
|
|
|
3411
3529
|
// Trigger callDataAvailable dummy event
|
|
3412
|
-
if (state ===
|
|
3413
|
-
this._callDisposition = this._manualDisposition ||
|
|
3530
|
+
if (state === 'TERMINATED') {
|
|
3531
|
+
this._callDisposition = this._manualDisposition || 'NORMAL';
|
|
3414
3532
|
this._callFinished();
|
|
3415
3533
|
}
|
|
3416
3534
|
|
|
3417
|
-
if (state ===
|
|
3418
|
-
if (info.cause ===
|
|
3419
|
-
this._callDisposition =
|
|
3535
|
+
if (state === 'FAILED') {
|
|
3536
|
+
if (info.cause === 'Unavailable') {
|
|
3537
|
+
this._callDisposition = 'TEMP_UNAVAIL';
|
|
3420
3538
|
if (this._phone.options.playSignalTones) {
|
|
3421
3539
|
this._phone._startRepeatTone(ST_TEMP_UNAVAIL);
|
|
3422
3540
|
}
|
|
3423
3541
|
}
|
|
3424
3542
|
|
|
3425
|
-
if (info.cause ===
|
|
3426
|
-
this._callDisposition =
|
|
3543
|
+
if (info.cause === 'Not Found') {
|
|
3544
|
+
this._callDisposition = 'INVALID_NUMBER';
|
|
3427
3545
|
if (this._phone.options.playSignalTones) {
|
|
3428
3546
|
this._phone._startRepeatTone(ST_INVALID_NUMBER);
|
|
3429
3547
|
}
|
|
3430
3548
|
}
|
|
3431
3549
|
|
|
3432
|
-
if (info.cause ===
|
|
3433
|
-
this._callDisposition = this._manualDisposition ||
|
|
3550
|
+
if (info.cause === 'Canceled') {
|
|
3551
|
+
this._callDisposition = this._manualDisposition || 'NO_ANSWER';
|
|
3434
3552
|
}
|
|
3435
3553
|
|
|
3436
|
-
if (info.cause ===
|
|
3437
|
-
if (this.direction ===
|
|
3438
|
-
this._callDisposition =
|
|
3554
|
+
if (info.cause === 'Rejected') {
|
|
3555
|
+
if (this.direction === 'INBOUND') {
|
|
3556
|
+
this._callDisposition = 'REJECTED';
|
|
3439
3557
|
} else {
|
|
3440
|
-
this._callDisposition =
|
|
3558
|
+
this._callDisposition = 'BARRED';
|
|
3441
3559
|
}
|
|
3442
3560
|
}
|
|
3443
3561
|
|
|
3444
|
-
if (info.cause ===
|
|
3445
|
-
this._callDisposition =
|
|
3562
|
+
if (info.cause === 'Busy') {
|
|
3563
|
+
this._callDisposition = 'BUSY';
|
|
3446
3564
|
if (this._phone.options.playSignalTones) {
|
|
3447
3565
|
this._phone._startRepeatTone(ST_BUSY);
|
|
3448
3566
|
}
|
|
3449
3567
|
}
|
|
3450
3568
|
|
|
3451
3569
|
// If session is incoming and failed, we don't fire calldataAvailable event
|
|
3452
|
-
if (this._webRTCSession.direction ===
|
|
3570
|
+
if (this._webRTCSession.direction === 'incoming') ;
|
|
3453
3571
|
|
|
3454
3572
|
this._callFinished();
|
|
3455
3573
|
}
|
|
@@ -3477,7 +3595,7 @@ class Session extends EventEmitter {
|
|
|
3477
3595
|
* @property {string} REJECTED Incoming call was rejected
|
|
3478
3596
|
*/
|
|
3479
3597
|
_callFinished() {
|
|
3480
|
-
this.log.info(
|
|
3598
|
+
this.log.info('Call finished');
|
|
3481
3599
|
|
|
3482
3600
|
// Fire callEnded event
|
|
3483
3601
|
this._fireCallEndedEvent();
|
|
@@ -3494,7 +3612,7 @@ class Session extends EventEmitter {
|
|
|
3494
3612
|
|
|
3495
3613
|
// Set answer call timeout
|
|
3496
3614
|
_setAnswerCallTimeout(timeout) {
|
|
3497
|
-
this.log.debug(
|
|
3615
|
+
this.log.debug('Setting answer call timeout to: ' + timeout);
|
|
3498
3616
|
this._answerCallTimeout = timeout;
|
|
3499
3617
|
}
|
|
3500
3618
|
|
|
@@ -3506,9 +3624,9 @@ class Session extends EventEmitter {
|
|
|
3506
3624
|
* @property {boolean} state - New recording state
|
|
3507
3625
|
*/
|
|
3508
3626
|
try {
|
|
3509
|
-
this.emit(
|
|
3627
|
+
this.emit('recordingStateChange', { state: this._isRecording, error: this._recordingError });
|
|
3510
3628
|
} catch (e) {
|
|
3511
|
-
this.log.error(
|
|
3629
|
+
this.log.error('Exception in recording state change event: ' + e);
|
|
3512
3630
|
}
|
|
3513
3631
|
}
|
|
3514
3632
|
|
|
@@ -3520,9 +3638,9 @@ class Session extends EventEmitter {
|
|
|
3520
3638
|
* @property {boolean} state - Mute state (true = muted / false = unmuted)
|
|
3521
3639
|
*/
|
|
3522
3640
|
try {
|
|
3523
|
-
this.emit(
|
|
3641
|
+
this.emit('muteStateChange', { state: this._isMuted });
|
|
3524
3642
|
} catch (e) {
|
|
3525
|
-
this.log.error(
|
|
3643
|
+
this.log.error('Exception in mute state change event: ' + e);
|
|
3526
3644
|
}
|
|
3527
3645
|
}
|
|
3528
3646
|
|
|
@@ -3534,9 +3652,9 @@ class Session extends EventEmitter {
|
|
|
3534
3652
|
* @property {boolean} state - New hold state
|
|
3535
3653
|
*/
|
|
3536
3654
|
try {
|
|
3537
|
-
this.emit(
|
|
3655
|
+
this.emit('holdStateChange', { state: this._isOnHold });
|
|
3538
3656
|
} catch (e) {
|
|
3539
|
-
this.log.error(
|
|
3657
|
+
this.log.error('Exception in hold state change event: ' + e);
|
|
3540
3658
|
}
|
|
3541
3659
|
}
|
|
3542
3660
|
|
|
@@ -3548,9 +3666,9 @@ class Session extends EventEmitter {
|
|
|
3548
3666
|
* @property {boolean} mode - New whisper mode (0 = No whisper / 1 = Local only / 2 = Three way)
|
|
3549
3667
|
*/
|
|
3550
3668
|
try {
|
|
3551
|
-
this.emit(
|
|
3669
|
+
this.emit('whisperModeChange', { mode: this._whisperMode });
|
|
3552
3670
|
} catch (e) {
|
|
3553
|
-
this.log.error(
|
|
3671
|
+
this.log.error('Exception in whisper mode change event: ' + e);
|
|
3554
3672
|
}
|
|
3555
3673
|
}
|
|
3556
3674
|
|
|
@@ -3599,14 +3717,14 @@ class Session extends EventEmitter {
|
|
|
3599
3717
|
|
|
3600
3718
|
// Add some extra convenience fields
|
|
3601
3719
|
e.durationStr = createDurationString(e.duration);
|
|
3602
|
-
e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) :
|
|
3603
|
-
e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) :
|
|
3720
|
+
e.answerDurationStr = e.answerDuration ? createDurationString(e.answerDuration) : '';
|
|
3721
|
+
e.activeDurationStr = e.activeDuration ? createDurationString(e.activeDuration) : '';
|
|
3604
3722
|
e.callQualityReport = this._currentReport;
|
|
3605
3723
|
|
|
3606
3724
|
try {
|
|
3607
|
-
this.emit(
|
|
3725
|
+
this.emit('callEnded', e);
|
|
3608
3726
|
} catch (e) {
|
|
3609
|
-
this.log.error(
|
|
3727
|
+
this.log.error('Exception in callEnded event: ' + e);
|
|
3610
3728
|
}
|
|
3611
3729
|
}
|
|
3612
3730
|
|
|
@@ -3617,11 +3735,11 @@ class Session extends EventEmitter {
|
|
|
3617
3735
|
|
|
3618
3736
|
const self = this;
|
|
3619
3737
|
session.connection.ontrack = function (event) {
|
|
3620
|
-
self.log.debug(
|
|
3738
|
+
self.log.debug('Start media output (track):');
|
|
3621
3739
|
self.log.debug(event);
|
|
3622
3740
|
self._phone._remoteAudioElement.srcObject = event.streams[0];
|
|
3623
3741
|
};
|
|
3624
3742
|
}
|
|
3625
3743
|
}
|
|
3626
3744
|
|
|
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 };
|
|
3745
|
+
export { EventClient, EventServiceNotConnected, InvalidMediaDevice, InvalidOptions, InvalidParameter, LOGLVL$1 as LOGLVL, LogHandler, MediaNotPrepared, NoActiveSession, NotAllowed, NotReady, Phone, Request, Requestor, Session, UnableToConnect, browserIsChrome, browserIsEdge, browserIsFirefox, browserIsOpera, browserIsSafari, buildCustomHeaders, compareObjects, createDurationString, createUUID };
|