@telegenta/webclient 2.0.12 → 3.0.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/README.md +102 -81
- package/demos/add_phone_number.html +8 -7
- package/demos/calldata_available.html +5 -5
- package/demos/embedded_config.html +3 -3
- package/demos/event_client.html +226 -211
- package/demos/phone_client.html +540 -514
- package/demos/styles.css +0 -0
- package/dist/telegenta.js +3663 -0
- package/dist/telegenta.min.js +1 -0
- package/docs/EventClient.html +3 -1093
- package/docs/EventServiceNotConnected.html +3 -0
- package/docs/InvalidMediaDevice.html +3 -0
- package/docs/InvalidOptions.html +3 -0
- package/docs/InvalidParameter.html +3 -0
- package/docs/LogHandler.html +7 -0
- package/docs/MediaNotPrepared.html +3 -0
- package/docs/NoActiveSession.html +3 -0
- package/docs/NotAllowed.html +3 -0
- package/docs/NotReady.html +3 -0
- package/docs/Phone.html +4 -2837
- package/docs/Request.html +3 -0
- package/docs/Requestor.html +3 -0
- package/docs/Session.html +3 -2599
- package/docs/UnableToConnect.html +3 -0
- package/docs/data/search.json +1 -0
- package/docs/event-client.js.html +352 -0
- package/docs/exceptions.js.html +104 -0
- package/docs/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/global.html +5 -4950
- package/docs/index.html +3 -66
- package/docs/logger.js.html +106 -0
- package/docs/phone-client.js.html +2175 -0
- package/docs/requestor.js.html +223 -0
- package/docs/scripts/core.js +726 -0
- package/docs/scripts/core.min.js +23 -0
- package/docs/scripts/resize.js +90 -0
- package/docs/scripts/search.js +265 -0
- package/docs/scripts/search.min.js +6 -0
- package/docs/scripts/{prettify → third-party}/Apache-License-2.0.txt +202 -202
- package/docs/scripts/third-party/fuse.js +9 -0
- package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
- package/docs/scripts/third-party/hljs-line-num.js +1 -0
- package/docs/scripts/third-party/hljs-original.js +5171 -0
- package/docs/scripts/third-party/hljs.js +1 -0
- package/docs/scripts/third-party/popper.js +5 -0
- package/docs/scripts/third-party/tippy.js +1 -0
- package/docs/scripts/third-party/tocbot.js +672 -0
- package/docs/scripts/third-party/tocbot.min.js +1 -0
- package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
- package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
- package/docs/styles/clean-jsdoc-theme-light.css +482 -0
- package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
- package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
- package/docs/styles/clean-jsdoc-theme.min.css +1 -0
- package/package.json +38 -26
- package/babel.config.js +0 -16
- package/browser/telegenta.min.js +0 -1
- package/demos/telegenta.min.js +0 -1
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/styles/jsdoc-default.css +0 -692
- package/docs/styles/prettify-jsdoc.css +0 -111
- package/docs/styles/prettify-tomorrow.css +0 -132
- package/embedded_client/embedded_phone.js +0 -325
- package/embedded_client/index.html +0 -201
- package/embedded_client/server.php +0 -28
- package/embedded_client/styles.css +0 -310
- package/embedded_client/tiny_rest_client.php +0 -73
- package/embedded_client/webphone_sprites.png +0 -0
- package/es5/event-client.js +0 -277
- package/es5/index.js +0 -44
- package/es5/phone-client.js +0 -1336
- package/src/event-client.js +0 -1
- package/src/index.js +0 -10
- package/src/phone-client.js +0 -1
- package/webpack.config.js +0 -39
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: phone-client.js</title><!--[if lt IE 9]>
|
|
2
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
3
|
+
<![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="EventClient.html">EventClient</a></div><div class="sidebar-section-children"><a href="EventServiceNotConnected.html">EventServiceNotConnected</a></div><div class="sidebar-section-children"><a href="InvalidMediaDevice.html">InvalidMediaDevice</a></div><div class="sidebar-section-children"><a href="InvalidOptions.html">InvalidOptions</a></div><div class="sidebar-section-children"><a href="InvalidParameter.html">InvalidParameter</a></div><div class="sidebar-section-children"><a href="LogHandler.html">LogHandler</a></div><div class="sidebar-section-children"><a href="MediaNotPrepared.html">MediaNotPrepared</a></div><div class="sidebar-section-children"><a href="NoActiveSession.html">NoActiveSession</a></div><div class="sidebar-section-children"><a href="NotAllowed.html">NotAllowed</a></div><div class="sidebar-section-children"><a href="NotReady.html">NotReady</a></div><div class="sidebar-section-children"><a href="Phone.html">Phone</a></div><div class="sidebar-section-children"><a href="Request.html">Request</a></div><div class="sidebar-section-children"><a href="Requestor.html">Requestor</a></div><div class="sidebar-section-children"><a href="Session.html">Session</a></div><div class="sidebar-section-children"><a href="UnableToConnect.html">UnableToConnect</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-events"><div>Events</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#event:callDataInfo">callDataInfo</a></div><div class="sidebar-section-children"><a href="global.html#event:callDurationUpdate">callDurationUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:callEnded">callEnded</a></div><div class="sidebar-section-children"><a href="global.html#event:callQualityReportUpdate">callQualityReportUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:connectionStateChange">connectionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:holdStateChange">holdStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:incomingCall">incomingCall</a></div><div class="sidebar-section-children"><a href="global.html#event:muteStateChange">muteStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:recordingStateChange">recordingStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionCreated">sessionCreated</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionRemoved">sessionRemoved</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionStateChange">sessionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:whisperModeChange">whisperModeChange</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CALL_DIRECTION">CALL_DIRECTION</a></div><div class="sidebar-section-children"><a href="global.html#CALL_DISPOSITION">CALL_DISPOSITION</a></div><div class="sidebar-section-children"><a href="global.html#CONNECTION_STATE">CONNECTION_STATE</a></div><div class="sidebar-section-children"><a href="global.html#LOGLVL">LOGLVL</a></div><div class="sidebar-section-children"><a href="global.html#SESSION_STATE">SESSION_STATE</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">phone-client.js</h1></header><article><pre class="prettyprint source lang-js"><code>// 'use strict';
|
|
4
|
+
import EventEmitter from 'events';
|
|
5
|
+
import JsSIP from 'jssip';
|
|
6
|
+
import { LogHandler, LOGLVL } from './logger';
|
|
7
|
+
import * as Exceptions from './exceptions';
|
|
8
|
+
import * as Tools from './tools';
|
|
9
|
+
import { EventClient } from './event-client';
|
|
10
|
+
import camelcaseKeys from 'camelcase-keys';
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
wssServer: 'wss://api.telegenta.com/sip/',
|
|
14
|
+
eventServer: 'wss://api.telegenta.com/es/',
|
|
15
|
+
provisionToken: null,
|
|
16
|
+
authorizationUsername: null,
|
|
17
|
+
password: null,
|
|
18
|
+
callToken: null,
|
|
19
|
+
register: true,
|
|
20
|
+
iceGatherTimeout: 3000,
|
|
21
|
+
playSignalTones: true,
|
|
22
|
+
logger: null,
|
|
23
|
+
logLevel: 3,
|
|
24
|
+
connectEventService: false,
|
|
25
|
+
useCallDataInfoEvent: false,
|
|
26
|
+
toneVolume: 0.3,
|
|
27
|
+
autoBusyIncoming: true,
|
|
28
|
+
autoRemoveSessionTime: 3000,
|
|
29
|
+
playRingOnIncoming: true,
|
|
30
|
+
playCallInitializedTone: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// DTMF Frequency table
|
|
34
|
+
const DTMF_FREQUENCY_TABLE = {
|
|
35
|
+
1: [1209, 697],
|
|
36
|
+
2: [1336, 697],
|
|
37
|
+
3: [1477, 697],
|
|
38
|
+
a: [1633, 697],
|
|
39
|
+
4: [1209, 770],
|
|
40
|
+
5: [1336, 770],
|
|
41
|
+
6: [1477, 770],
|
|
42
|
+
b: [1633, 770],
|
|
43
|
+
7: [1209, 852],
|
|
44
|
+
8: [1336, 852],
|
|
45
|
+
9: [1477, 852],
|
|
46
|
+
c: [1633, 852],
|
|
47
|
+
'*': [1209, 941],
|
|
48
|
+
0: [1336, 941],
|
|
49
|
+
'#': [1477, 941],
|
|
50
|
+
d: [1633, 941],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// SIGNAL TONE SEQUENCES
|
|
54
|
+
|
|
55
|
+
// Params: d = Duration, f1 = Frequency 1, f2 = Frequency 2, g = Gap
|
|
56
|
+
const ST_TEMP_UNAVAIL = [{ d: 250, f1: 425, f2: 0, g: 250 }];
|
|
57
|
+
const ST_BUSY = [{ d: 500, f1: 425, f2: 0, g: 500 }];
|
|
58
|
+
const ST_INVALID_NUMBER = [
|
|
59
|
+
{ d: 333, f1: 950, f2: 0, g: 0 },
|
|
60
|
+
{ d: 333, f1: 1400, f2: 0, g: 0 },
|
|
61
|
+
{ d: 333, f1: 1800, f2: 0, g: 1000 },
|
|
62
|
+
];
|
|
63
|
+
const ST_CALLINIT = [
|
|
64
|
+
{ d: 200, f1: 500, f2: 0, g: 0 },
|
|
65
|
+
{ d: 200, f1: 700, f2: 0, g: 5000 },
|
|
66
|
+
];
|
|
67
|
+
const ST_RINGBACK = [{ d: 1000, f1: 425, f2: 0, g: 4000 }];
|
|
68
|
+
const ST_INCOMING = [
|
|
69
|
+
{ d: 150, f1: 400, f2: 0, g: 0 },
|
|
70
|
+
{ d: 150, f1: 600, f2: 0, g: 0 },
|
|
71
|
+
{ d: 150, f1: 400, f2: 0, g: 0 },
|
|
72
|
+
{ d: 150, f1: 600, f2: 0, g: 0 },
|
|
73
|
+
{ d: 150, f1: 400, f2: 0, g: 0 },
|
|
74
|
+
{ d: 150, f1: 600, f2: 0, g: 3000 },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This class acts as a SIP user Agent and is responsible for handling calls using the WebRTC interface in the browser.<br>
|
|
79
|
+
* 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
|
|
80
|
+
* instance. See {@link LogHandler} class for more information.
|
|
81
|
+
*
|
|
82
|
+
* @param {LogHandler} [logger] - Custom log instance
|
|
83
|
+
*
|
|
84
|
+
* @fires connectionStateChange on connection state changes.
|
|
85
|
+
* @fires sessionCreated When a new session has been created
|
|
86
|
+
* @fires sessionRemoved When a existing session has been removed (after autoRemoveSessionTime)
|
|
87
|
+
* @fires callDataInfo Receive detailed information about call after call has been terminated
|
|
88
|
+
* @fires incomingCall Incoming call sessions are received on this event
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Creating Phone object and use it make a call
|
|
92
|
+
* const phone = new Telegenta.Phone()
|
|
93
|
+
* phone.prepareMedia().then(() => {
|
|
94
|
+
* // Hook up events
|
|
95
|
+
* ...
|
|
96
|
+
* // Start phone
|
|
97
|
+
* phone.start(options);
|
|
98
|
+
*
|
|
99
|
+
* // Dial 12345678
|
|
100
|
+
* session = phone.call('12345678');
|
|
101
|
+
* }
|
|
102
|
+
*/
|
|
103
|
+
export class Phone extends EventEmitter {
|
|
104
|
+
constructor(logger = null) {
|
|
105
|
+
super();
|
|
106
|
+
|
|
107
|
+
this._mediaPermission = false;
|
|
108
|
+
this._mediaPermissionError = false;
|
|
109
|
+
this._UA = null;
|
|
110
|
+
this._connectionState = null;
|
|
111
|
+
this._isStarted = false;
|
|
112
|
+
this._eventClient = null;
|
|
113
|
+
this._options = null;
|
|
114
|
+
|
|
115
|
+
// List of active sessions
|
|
116
|
+
this._sessionCounter = 0;
|
|
117
|
+
this._activeSessions = {};
|
|
118
|
+
|
|
119
|
+
// Create remote audio element
|
|
120
|
+
this._remoteAudioElement = document.createElement('AUDIO');
|
|
121
|
+
this._remoteAudioElement.autoplay = true;
|
|
122
|
+
|
|
123
|
+
// Tone generator
|
|
124
|
+
this._toneAudioElement = document.createElement('AUDIO');
|
|
125
|
+
this._toneAudioElement.autoplay = true;
|
|
126
|
+
this._toneTimer = null;
|
|
127
|
+
this._repeatToneActive = false;
|
|
128
|
+
this._repeatToneSequence = [];
|
|
129
|
+
this._repeatToneSequencePos = 0;
|
|
130
|
+
this._toneGainProcessor = null;
|
|
131
|
+
this._oscillator1 = null;
|
|
132
|
+
this._oscillator2 = null;
|
|
133
|
+
|
|
134
|
+
// Set default logger
|
|
135
|
+
this.log = logger ? logger : new LogHandler(LOGLVL.INFO);
|
|
136
|
+
|
|
137
|
+
// Handle jssip debug logs
|
|
138
|
+
if (this.log.level === LOGLVL.DEBUG) {
|
|
139
|
+
JsSIP.debug.disable('JsSIP:*');
|
|
140
|
+
} else {
|
|
141
|
+
JsSIP.debug.disable('JsSIP:*');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Prepare media and get media permission from browser
|
|
147
|
+
* This must be done prior to using the phone or getting device lists.
|
|
148
|
+
*
|
|
149
|
+
* @return A promise which will be resolved when user accepts access to media devices
|
|
150
|
+
*
|
|
151
|
+
*/
|
|
152
|
+
prepareMedia() {
|
|
153
|
+
this.log.info('Get media permission');
|
|
154
|
+
const self = this;
|
|
155
|
+
const constraints = { audio: true, video: false };
|
|
156
|
+
return new Promise(function (resolve, reject) {
|
|
157
|
+
navigator.mediaDevices.getUserMedia(constraints).then(
|
|
158
|
+
() => {
|
|
159
|
+
self._mediaPermission = true;
|
|
160
|
+
self._mediaPermissionError = false;
|
|
161
|
+
self.log.info('Media permission granted');
|
|
162
|
+
resolve();
|
|
163
|
+
},
|
|
164
|
+
(err) => {
|
|
165
|
+
self._mediaPermission = false;
|
|
166
|
+
self._mediaPermissionError = true;
|
|
167
|
+
self.log.error('Denied media permission: ' + err);
|
|
168
|
+
reject(err);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start phone and connect to server if needed
|
|
176
|
+
*
|
|
177
|
+
* @param phoneOptions {object} configuration options
|
|
178
|
+
* @param {string} [phoneOptions.provisionToken] Provisioning token from API service
|
|
179
|
+
* @param {string} [phoneOptions.authorizationUsername] Authorizing username
|
|
180
|
+
* @param {string} [phoneOptions.password] WSS Password
|
|
181
|
+
* @param {string} [phoneOptions.callToken] Call token
|
|
182
|
+
* @param {boolean} [phoneOptions.connectEventService=false] Connect to event service (required to send call commands and receive callDataInfo event)
|
|
183
|
+
* @param {boolean} [phoneOptions.useCallDataInfoEvent=false] Subscribe to callDataInfo remote event
|
|
184
|
+
* @param {boolean} [phoneOptions.register=true] Register on server to receive enable incoming calls
|
|
185
|
+
* @param {number} [phoneOptions.playSignalTones=false] Play signal tones (busy, invalid number, etc) remotely
|
|
186
|
+
* @param {number} [phoneOptions.iceGatherTimeout=3000] Number of millisecs before ICE gather will be forced stopped and candidate selection will take place
|
|
187
|
+
* @param {number} [phoneOptions.toneVolume=0.3] Signal- and DTMF tone volume (0-1)
|
|
188
|
+
* @param {boolean} [phoneOptions.autoBusyIncoming=true] Automatically reject calls with busy if active session exists
|
|
189
|
+
* @param {number} [phoneOptions.autoRemoveSessionTime=3000] Automatically remove session after interval specified
|
|
190
|
+
* @param {number} [phoneOptions.playRingOnIncoming=true] Play ringing tones on incoming calls
|
|
191
|
+
* @param {number} [phoneOptions.playCallInitializedTone=false] Play tone when call has been initiated (before progress/ringback tones)
|
|
192
|
+
*
|
|
193
|
+
* @throws MediaNotPrepared - If {@link prepareMedia} was not called and resolved prior to calling this method
|
|
194
|
+
* @throws InvalidOptions - If one or more options are invalid
|
|
195
|
+
* @throws UnableToConnect - If unable to connect phone to server
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
start(phoneOptions) {
|
|
199
|
+
this._checkMediaPermission();
|
|
200
|
+
// Check phone is not running already, if so stop it first
|
|
201
|
+
if (this._isStarted) {
|
|
202
|
+
this.log.warning('Phone already started, stopping first');
|
|
203
|
+
this.stop();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Apply options
|
|
207
|
+
this._options = Object.assign(options, phoneOptions);
|
|
208
|
+
|
|
209
|
+
this._initToneGenerator();
|
|
210
|
+
|
|
211
|
+
// Unpack provisioning token if specified
|
|
212
|
+
if (this._options.provisionToken) {
|
|
213
|
+
let pt = null;
|
|
214
|
+
try {
|
|
215
|
+
pt = JSON.parse(atob(this._options.provisionToken));
|
|
216
|
+
} catch (err) {
|
|
217
|
+
throw new Exceptions.InvalidOptions('provisionToken', 'Invalid token');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.log.info('Using provisioning token authorization');
|
|
221
|
+
this._options.wssServer = pt.wss_server;
|
|
222
|
+
this._options.eventServer = pt.event_server;
|
|
223
|
+
this._options.authorizationUsername = pt.wss_username;
|
|
224
|
+
this._options.password = pt.wss_password;
|
|
225
|
+
this._options.callToken = pt.wss_token;
|
|
226
|
+
} else {
|
|
227
|
+
this.log.info('Using manual specified credentials');
|
|
228
|
+
|
|
229
|
+
// Check auth info is provided
|
|
230
|
+
if (!this._options.authorizationUsername) {
|
|
231
|
+
throw new Exceptions.InvalidOptions('authorizationUsername', 'Missing username');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!this._options.password) {
|
|
235
|
+
throw new Exceptions.InvalidOptions('password', 'Missing password');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this._options.callToken) {
|
|
239
|
+
throw new Exceptions.InvalidOptions('callToken', 'Missing call token');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If we must subscribe to call data info event, we enable event service
|
|
244
|
+
if (this._options.useCallDataInfoEvent && !this._options.connectEventService) {
|
|
245
|
+
this.log.info(`Enabled event service because 'useCallDataInfoEvent' is enabled`);
|
|
246
|
+
this._options.connectEventService = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Connect to event service if needed
|
|
250
|
+
if (this._options.connectEventService) {
|
|
251
|
+
// Create and connect event client
|
|
252
|
+
this._eventClient = new EventClient(this.log);
|
|
253
|
+
|
|
254
|
+
this._eventClient.connect(this._options.eventServer).then(
|
|
255
|
+
() => {
|
|
256
|
+
this.log.info(`Connected to event server`);
|
|
257
|
+
|
|
258
|
+
// Subscript to CallDataEvent is needed
|
|
259
|
+
if (this._options.useCallDataInfoEvent) {
|
|
260
|
+
// Subscribe to calldata_available events
|
|
261
|
+
let eventSpec = {
|
|
262
|
+
event_name: 'calldata_available',
|
|
263
|
+
call_token: this._options.callToken,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
this._eventClient
|
|
267
|
+
.subscribe(eventSpec, (data) => {
|
|
268
|
+
try {
|
|
269
|
+
this.log.debug('Received remote call data info');
|
|
270
|
+
this.log.debug(data);
|
|
271
|
+
this._fireCallDataInfoEvent(data);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
this.log.error('Event callback failed: ' + e);
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
.then(
|
|
277
|
+
(subscriptionId) => {
|
|
278
|
+
this.log.info(`Subscription successful ID = ${subscriptionId}`);
|
|
279
|
+
},
|
|
280
|
+
(reason) => {
|
|
281
|
+
this.log.error(`Subscribe failed: ${reason}`);
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
(reason) => {
|
|
287
|
+
this.log.error(`Unable to connect to event server: ${reason}`);
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let wsSocket = null;
|
|
293
|
+
try {
|
|
294
|
+
wsSocket = new JsSIP.WebSocketInterface(this._options.wssServer);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw new Exceptions.UnableToConnect('Unable to connect phone websocket: ' + err);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.log.info('Starting phone');
|
|
300
|
+
this.log.debug(this._options);
|
|
301
|
+
|
|
302
|
+
this._UA = new JsSIP.UA({
|
|
303
|
+
sockets: [wsSocket],
|
|
304
|
+
authorization_user: this._options.authorizationUsername,
|
|
305
|
+
password: this._options.password,
|
|
306
|
+
register: this._options.register,
|
|
307
|
+
display_name: this._options.authorizationUsername,
|
|
308
|
+
uri: this._options.authorizationUsername + '@telegenta.com',
|
|
309
|
+
session_timers: false,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Setup remote media handler
|
|
313
|
+
const self = this;
|
|
314
|
+
this._UA.on('newRTCSession', function (data) {
|
|
315
|
+
self.log.debug('New RTCSession');
|
|
316
|
+
if (data.originator === 'remote') {
|
|
317
|
+
// Create new session
|
|
318
|
+
const session = new Session(self);
|
|
319
|
+
|
|
320
|
+
session.newInboundSession(data);
|
|
321
|
+
|
|
322
|
+
if (self._options.playRingOnIncoming) {
|
|
323
|
+
self._startRepeatTone(ST_INCOMING);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if we have other sessions and reject call if autoBusyIncoming is true
|
|
327
|
+
if (self._options.autoBusyIncoming) {
|
|
328
|
+
if (Object.keys(self._activeSessions).length > 0) {
|
|
329
|
+
self.log.info(
|
|
330
|
+
`Autorejecting incoming call ${session.uuid} because there is already active calls`,
|
|
331
|
+
);
|
|
332
|
+
session.reject(486, 'Busy');
|
|
333
|
+
return;
|
|
334
|
+
} else {
|
|
335
|
+
self.log.debug('There are no other sessions, adding incoming call');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
self._addSession(session);
|
|
340
|
+
|
|
341
|
+
// Set new session state
|
|
342
|
+
session._setSessionState('INCOMING');
|
|
343
|
+
|
|
344
|
+
// Now fire incomingCall event
|
|
345
|
+
self._fireIncomingCallEvent(session);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Setup connection state event subscriptions
|
|
350
|
+
this._UA.on('connecting', function (/* data */) {
|
|
351
|
+
self._setConnectionState('CONNECTING');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
this._UA.on('connected', function (/* data */) {
|
|
355
|
+
self._setConnectionState('CONNECTED');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
this._UA.on('disconnected', function (/* data */) {
|
|
359
|
+
self._setConnectionState('DISCONNECTED');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this._UA.on('registered', function (/* data */) {
|
|
363
|
+
self._setConnectionState('REGISTERED');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
this._UA.on('unregistered', function (/* data */) {
|
|
367
|
+
self._setConnectionState('UNREGISTERED');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
this._UA.on('registrationFailed', function (/* data */) {
|
|
371
|
+
self._setConnectionState('REGISTRATION_FAILED');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
this._UA.start();
|
|
375
|
+
this._isStarted = true;
|
|
376
|
+
this.log.info('Successfully started phone');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Stops the phone client, gracefully disconnects from the server and terminates any active calls
|
|
381
|
+
* @throws NotReady - If phone is not started
|
|
382
|
+
*/
|
|
383
|
+
stop() {
|
|
384
|
+
// Check phone is started first
|
|
385
|
+
if (!this._isStarted) {
|
|
386
|
+
throw new Exceptions.NotReady('Phone is not started');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Make sure events are not firing anymore, we can't rely on GC.
|
|
390
|
+
this._UA.removeAllListeners();
|
|
391
|
+
|
|
392
|
+
// Stop the UA
|
|
393
|
+
this._UA.stop();
|
|
394
|
+
delete this._UA;
|
|
395
|
+
|
|
396
|
+
// Disconnect event client if needed
|
|
397
|
+
if (!this._eventClient) {
|
|
398
|
+
this.log.debug('Event client not running, not stopping');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
this._eventClient.disconnect();
|
|
403
|
+
delete this._eventClient;
|
|
404
|
+
} catch (e) {
|
|
405
|
+
this.log.warning('Unable to disconnect event client');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this._isStarted = false;
|
|
409
|
+
this.log.info('Phone client successfully stopped');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Start a new call
|
|
414
|
+
* If you have other active calls they will be set on hold
|
|
415
|
+
* @param number Number to call
|
|
416
|
+
* @param params {object} call options
|
|
417
|
+
* @param {number} [params.record] Recording retention days. The number of days recording will be kept. Setting this to 0 disables recording.
|
|
418
|
+
* @param {number} [params.explicitCallerId] Force a specific caller id, by providing explicit phone number id
|
|
419
|
+
* @param {number} [params.explicitShortCallerId] Force a specific caller id, by providing explicit short caller id
|
|
420
|
+
* @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.
|
|
421
|
+
* @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.
|
|
422
|
+
* @param {string} [params.metaInfo] Add meta info to call, which will be available in server events and inside call complete webhook (max 256 chars)
|
|
423
|
+
* @return {Session} object for call
|
|
424
|
+
* @throws NotReady - If phone is not ready to call
|
|
425
|
+
* @throws NotAllowed - If there are calls currently connecting
|
|
426
|
+
*
|
|
427
|
+
*/
|
|
428
|
+
call(number, params = {}) {
|
|
429
|
+
// Check phone is started first
|
|
430
|
+
if (!this._isStarted) {
|
|
431
|
+
throw new Exceptions.NotReady('Phone is not started');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check we dont have any other connecting calls
|
|
435
|
+
if (this._checkForConnectingCalls()) {
|
|
436
|
+
throw new Exceptions.NotAllowed('Unable to make new calls while others are connecting');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Hold other calls if needed
|
|
440
|
+
this.holdAllCalls();
|
|
441
|
+
|
|
442
|
+
const callParams = Object.assign(
|
|
443
|
+
{
|
|
444
|
+
record: false,
|
|
445
|
+
maximumCallCost: 0,
|
|
446
|
+
answerCallTimeout: 0,
|
|
447
|
+
explicitCallerId: null,
|
|
448
|
+
explicitShortCallerId: null,
|
|
449
|
+
metaInfo: null,
|
|
450
|
+
_isListenCall: false,
|
|
451
|
+
},
|
|
452
|
+
params,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Trim phone number to remove spaces and '()'
|
|
456
|
+
number = number.trim().replace(/ |\(|\)/g, '');
|
|
457
|
+
|
|
458
|
+
this.log.info('Calling number: ' + number);
|
|
459
|
+
|
|
460
|
+
let session = new Session(this);
|
|
461
|
+
session.newOutboundSession(number, callParams);
|
|
462
|
+
this._addSession(session);
|
|
463
|
+
return session;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Start a new session and listen to another call
|
|
468
|
+
* @param listenKey provided by API endpoint "active-calls" and the event library subscription for resource name "activecall"
|
|
469
|
+
* @return {Session} object for listen
|
|
470
|
+
* @throws NotReady - If phone is not ready to call
|
|
471
|
+
* @throws NotAllowed - If there are calls currently connecting
|
|
472
|
+
*/
|
|
473
|
+
listen(listenKey) {
|
|
474
|
+
const s = listenKey.split('@');
|
|
475
|
+
let callUUID = s[0];
|
|
476
|
+
let mediaSwitch = 'sip:' + s[1];
|
|
477
|
+
|
|
478
|
+
this.log.debug('Initiating listen to call with uuid ' + callUUID + ' on media switch ' + mediaSwitch);
|
|
479
|
+
return this.call('listen-' + callUUID, {
|
|
480
|
+
mediaSwitchURI: mediaSwitch,
|
|
481
|
+
_isListenCall: true,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get session by UUID, if none are found null is returned
|
|
487
|
+
* @param uuid
|
|
488
|
+
* @returns {Session|null}
|
|
489
|
+
*/
|
|
490
|
+
getSessionByUUID(uuid) {
|
|
491
|
+
for (let key in this._activeSessions) {
|
|
492
|
+
if (this._activeSessions.hasOwnProperty(key)) {
|
|
493
|
+
if (this._activeSessions[key].uuid === uuid) {
|
|
494
|
+
return this._activeSessions[key];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Return phone options
|
|
504
|
+
* @returns {phoneOptions}
|
|
505
|
+
*/
|
|
506
|
+
get options() {
|
|
507
|
+
return this._options;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Return list of current sessions
|
|
512
|
+
* @returns [Session]
|
|
513
|
+
*/
|
|
514
|
+
get sessions() {
|
|
515
|
+
let s = [];
|
|
516
|
+
for (let key in this._activeSessions) {
|
|
517
|
+
if (this._activeSessions.hasOwnProperty(key)) {
|
|
518
|
+
s.push(this._activeSessions[key]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return s;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Return media permission state
|
|
527
|
+
* @return {boolean}
|
|
528
|
+
*/
|
|
529
|
+
get hasMediaPermission() {
|
|
530
|
+
return this._mediaPermission;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Media permission denied
|
|
535
|
+
* @return {boolean}
|
|
536
|
+
*/
|
|
537
|
+
get hasMediaPermissionError() {
|
|
538
|
+
return this._mediaPermissionError;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Phone client started
|
|
543
|
+
* @return {boolean}
|
|
544
|
+
*/
|
|
545
|
+
get isStarted() {
|
|
546
|
+
return this._isStarted;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Verify browser is supported
|
|
551
|
+
* @return true if browser supported
|
|
552
|
+
*/
|
|
553
|
+
get browserSupported() {
|
|
554
|
+
return Tools.browserIsChrome() || Tools.browserIsFirefox() || Tools.browserIsOpera() || Tools.browserIsSafari();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Remote audio HTML element
|
|
559
|
+
*/
|
|
560
|
+
get remoteAudioElement() {
|
|
561
|
+
return this._remoteAudioElement;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// TONE STUFF
|
|
565
|
+
/**
|
|
566
|
+
* Play tone locally (on incoming remote audio)
|
|
567
|
+
* @param duration Duration of tone in ms
|
|
568
|
+
* @param freq1 Frequency in Hz of tone 1
|
|
569
|
+
* @param freq2 Frequency in Hz of tone 2
|
|
570
|
+
* @param callback Function to be called after tone has ended
|
|
571
|
+
* @throws NotAllowed - If audio api is not supported by browser
|
|
572
|
+
*/
|
|
573
|
+
playTone(duration, freq1, freq2 = 0, callback = null) {
|
|
574
|
+
this.log.debug(`Playing tone ${freq1} / ${freq2} for ${duration} ms`);
|
|
575
|
+
|
|
576
|
+
this._oscillator1.frequency.value = freq1;
|
|
577
|
+
this._oscillator2.frequency.value = freq2;
|
|
578
|
+
this._toneGainProcessor.gain.value = this._options.toneVolume;
|
|
579
|
+
|
|
580
|
+
// Set timer to stop tone again
|
|
581
|
+
const self = this;
|
|
582
|
+
this._toneTimer = setTimeout(function () {
|
|
583
|
+
self.stopTone();
|
|
584
|
+
|
|
585
|
+
// Handle callback now
|
|
586
|
+
if (callback) {
|
|
587
|
+
callback();
|
|
588
|
+
}
|
|
589
|
+
}, duration);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Immediately stop playing tones
|
|
594
|
+
* (Tone callbacks will be cancelled)
|
|
595
|
+
*/
|
|
596
|
+
stopTone() {
|
|
597
|
+
this._oscillator1.frequency.value = 0;
|
|
598
|
+
this._oscillator2.frequency.value = 0;
|
|
599
|
+
this._toneGainProcessor.gain.value = 0;
|
|
600
|
+
clearTimeout(this._toneTimer);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Begin repeating tones
|
|
604
|
+
_startRepeatTone(sequences) {
|
|
605
|
+
this.log.info('Repeating tone sequence started');
|
|
606
|
+
this.log.debug(sequences);
|
|
607
|
+
this._repeatToneSequence = sequences;
|
|
608
|
+
this._repeatToneSequencePos = 0;
|
|
609
|
+
this._repeatToneActive = true;
|
|
610
|
+
this._doRepeatTone();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Do one tone
|
|
614
|
+
_doRepeatTone() {
|
|
615
|
+
if (this._repeatToneActive) {
|
|
616
|
+
this.log.debug(`Playing repeated tone, pos = ${this._repeatToneSequencePos}`);
|
|
617
|
+
|
|
618
|
+
// Get tone definitions and move to next sequence position
|
|
619
|
+
let toneDef = this._repeatToneSequence[this._repeatToneSequencePos];
|
|
620
|
+
this._repeatToneSequencePos++;
|
|
621
|
+
if (this._repeatToneSequencePos === this._repeatToneSequence.length) {
|
|
622
|
+
this._repeatToneSequencePos = 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const self = this;
|
|
626
|
+
this.playTone(toneDef.d, toneDef.f1, toneDef.f2, function () {
|
|
627
|
+
setTimeout(self._doRepeatTone.bind(self), toneDef.g);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// End repeating tones
|
|
633
|
+
_endRepeatTone() {
|
|
634
|
+
if (this._repeatToneActive) {
|
|
635
|
+
this.log.info('Repeating tone stopped');
|
|
636
|
+
this._repeatToneActive = false;
|
|
637
|
+
this.stopTone();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Initialize tone generator
|
|
642
|
+
_initToneGenerator() {
|
|
643
|
+
// Create audio context
|
|
644
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext || false;
|
|
645
|
+
if (!AudioContext) {
|
|
646
|
+
throw new Exceptions.NotAllowed('Audio API not supported by this browser');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const ctx = new AudioContext();
|
|
650
|
+
this._toneStreamNode = ctx.createMediaStreamDestination();
|
|
651
|
+
this._toneAudioElement.srcObject = this._toneStreamNode.stream;
|
|
652
|
+
|
|
653
|
+
this._toneGainProcessor = ctx.createGain();
|
|
654
|
+
this._toneGainProcessor.gain.value = 0;
|
|
655
|
+
this._toneGainProcessor.connect(this._toneStreamNode);
|
|
656
|
+
|
|
657
|
+
// Create two oscillators and start them
|
|
658
|
+
this._oscillator1 = ctx.createOscillator();
|
|
659
|
+
this._oscillator1.connect(this._toneGainProcessor);
|
|
660
|
+
this._oscillator1.frequency.value = 0;
|
|
661
|
+
this._oscillator1.start();
|
|
662
|
+
|
|
663
|
+
this._oscillator2 = ctx.createOscillator();
|
|
664
|
+
this._oscillator2.connect(this._toneGainProcessor);
|
|
665
|
+
this._oscillator2.frequency.value = 0;
|
|
666
|
+
this._oscillator2.start();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Hold all active calls
|
|
671
|
+
* @throws NotAllowed If there are connection calls
|
|
672
|
+
*/
|
|
673
|
+
holdAllCalls() {
|
|
674
|
+
this.log.info('Holding all active sessions');
|
|
675
|
+
// Check all session can be set on hold
|
|
676
|
+
if (this._checkForConnectingCalls()) {
|
|
677
|
+
throw new Exceptions.NotAllowed('A session is currently connecting');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Iterate through all active calls and hold them
|
|
681
|
+
for (let key in this._activeSessions) {
|
|
682
|
+
if (this._activeSessions.hasOwnProperty(key)) {
|
|
683
|
+
let session = this._activeSessions[key];
|
|
684
|
+
if (session.state === 'ACTIVE') {
|
|
685
|
+
session.hold();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/*
|
|
692
|
+
* ======= PRIVATE METHODS PHONE CLASS =========================================================================
|
|
693
|
+
*/
|
|
694
|
+
|
|
695
|
+
// Add a new session
|
|
696
|
+
_addSession(session) {
|
|
697
|
+
/**
|
|
698
|
+
* @event sessionCreated
|
|
699
|
+
* @type {object}
|
|
700
|
+
* @param {session} session - New session created
|
|
701
|
+
*/
|
|
702
|
+
// Give session unique id
|
|
703
|
+
this._sessionCounter++;
|
|
704
|
+
session.__containerId = this._sessionCounter;
|
|
705
|
+
|
|
706
|
+
this.log.info(`Adding new session ${session.__containerId}: ${session.uuid}`);
|
|
707
|
+
this._activeSessions[session.__containerId] = session;
|
|
708
|
+
this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions:`);
|
|
709
|
+
this.log.debug(this._activeSessions);
|
|
710
|
+
|
|
711
|
+
// Fire event
|
|
712
|
+
try {
|
|
713
|
+
this.emit('sessionCreated', { session: session });
|
|
714
|
+
} catch (e) {
|
|
715
|
+
this.log.error('Exception in sessionCreated event: ' + e);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Remove session
|
|
720
|
+
_removeSession(session) {
|
|
721
|
+
/**
|
|
722
|
+
* @event sessionRemoved
|
|
723
|
+
* @type {object}
|
|
724
|
+
* @param {session} session - Session removed
|
|
725
|
+
*/
|
|
726
|
+
|
|
727
|
+
this.log.info(`Removing session ${session.__containerId}: ${session.uuid}`);
|
|
728
|
+
|
|
729
|
+
// Fire event
|
|
730
|
+
try {
|
|
731
|
+
this.emit('sessionRemoved', { session: session });
|
|
732
|
+
} catch (e) {
|
|
733
|
+
this.log.error('Exception in sessionRemoved event: ' + e);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
delete this._activeSessions[session.__containerId];
|
|
737
|
+
this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions`);
|
|
738
|
+
this.log.debug(this._activeSessions);
|
|
739
|
+
|
|
740
|
+
// Stop any repeating tones
|
|
741
|
+
this._endRepeatTone();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Session call this when call is finished
|
|
745
|
+
_sessionTerminated(session) {
|
|
746
|
+
this.log.debug(`Session ${session.uuid} terminated, removing in ${this._options.autoRemoveSessionTime} ms`);
|
|
747
|
+
const self = this;
|
|
748
|
+
setTimeout(function () {
|
|
749
|
+
self._removeSession(session);
|
|
750
|
+
}, this._options.autoRemoveSessionTime);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Return true if there are calls currently connecting
|
|
754
|
+
_checkForConnectingCalls() {
|
|
755
|
+
for (let key in this._activeSessions) {
|
|
756
|
+
if (this._activeSessions.hasOwnProperty(key)) {
|
|
757
|
+
let session = this._activeSessions[key];
|
|
758
|
+
if (
|
|
759
|
+
session.state === 'INITIALIZING' ||
|
|
760
|
+
session.state === 'CALLING' ||
|
|
761
|
+
session.state === 'PROGRESS' ||
|
|
762
|
+
session.state === 'INCOMING'
|
|
763
|
+
) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* @typedef {string} CONNECTION_STATE
|
|
773
|
+
* @property {string} CONNECTING Connecting to server
|
|
774
|
+
* @property {string} CONNECTED Connected to server
|
|
775
|
+
* @property {string} DISCONNECTED Disconnected from server
|
|
776
|
+
* @property {string} REGISTERED Registered on server
|
|
777
|
+
* @property {string} UNREGISTERED De-registered from server
|
|
778
|
+
* @property {string} REGISTRATION_FAILED Registration failed
|
|
779
|
+
*/
|
|
780
|
+
// Set connection state and emit event
|
|
781
|
+
_setConnectionState(state) {
|
|
782
|
+
/**
|
|
783
|
+
* @event connectionStateChange
|
|
784
|
+
* @type {object}
|
|
785
|
+
* @param {CONNECTION_STATE} state - The current connection state
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
this.log.info('Connection state: ' + state);
|
|
789
|
+
this._connectionState = state;
|
|
790
|
+
try {
|
|
791
|
+
this.emit('connectionStateChange', { state: state });
|
|
792
|
+
} catch (e) {
|
|
793
|
+
this.log.error('Exception in connectionStateChange event: ' + e);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @event incomingCall
|
|
799
|
+
* @type {object}
|
|
800
|
+
* @param {Session} session New incoming session
|
|
801
|
+
*/
|
|
802
|
+
_fireIncomingCallEvent(session) {
|
|
803
|
+
try {
|
|
804
|
+
this.emit('incomingCall', session);
|
|
805
|
+
} catch (e) {
|
|
806
|
+
this.log.error('Exception in incomingCall event: ' + e);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Fire call data event to subscribers
|
|
811
|
+
_fireCallDataInfoEvent(eventData = null) {
|
|
812
|
+
/**
|
|
813
|
+
* @event callDataInfo
|
|
814
|
+
*
|
|
815
|
+
* @type {object}
|
|
816
|
+
* @property {uuid} uuid - Customer Call UUID for session
|
|
817
|
+
* @param {CALL_DIRECTION} direction - Call direction
|
|
818
|
+
* @param {CALL_DISPOSITION} disposition - The final call disposition
|
|
819
|
+
* @param {string} originatingNumber - Caller ID number (a-number) used for call
|
|
820
|
+
* @param {string} destinationNumber - Destination number (b-number) used for call
|
|
821
|
+
* @param {Date} startTime - Start time of call
|
|
822
|
+
* @param {Date} answerTime - Time call was answered
|
|
823
|
+
* @param {Date} endTime - End time of call
|
|
824
|
+
* @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
|
|
825
|
+
* @param {String} durationStr - Total duration of the call formatted as (mm:ss)
|
|
826
|
+
* @param {number} answerDuration - Time before call was answered (millisec)
|
|
827
|
+
* @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
|
|
828
|
+
* @param {number} activeDuration - Active duration of the call (time spend talking) (millisec)
|
|
829
|
+
* @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
|
|
830
|
+
* @param {number} rateDestinationId - Rate destination id
|
|
831
|
+
* @param {string} rateDestinationStr - Rate destination name
|
|
832
|
+
* @param {number} rateDestinationCost - Rate destination cost / minute
|
|
833
|
+
* @param {number} callConnectFee - Call attempt/connect cost
|
|
834
|
+
* @param {number} effectiveCidId - Phone number id used as a-number for call
|
|
835
|
+
* @param {number} effectiveCidNumberStr - Phone number used as a-number for call
|
|
836
|
+
* @param {string} effectiveCidNameStr - Phone number name used as a-number for call
|
|
837
|
+
* @param {boolean} callPrivacy - Call privacy used for call (number hidden)
|
|
838
|
+
* @param {number} hangupCauseQ850 - Q.850 hangup cause code
|
|
839
|
+
* @param {Object[]} recordings - List of recordings done for this call
|
|
840
|
+
* @param {number} recordings[].startTime - Time recording was started
|
|
841
|
+
* @param {number} recordings[].length - Recording length in milliseconds
|
|
842
|
+
* @param {number} recordings[].size - Recording size (bytes)
|
|
843
|
+
* @param {number} recordings[].urlPlayer - URL for recording with player
|
|
844
|
+
* @param {number} recordings[].url - URL for recording without player
|
|
845
|
+
* @param {number} recordings[].retentionDate - Date recording will be automatically deleted
|
|
846
|
+
*/
|
|
847
|
+
|
|
848
|
+
let e;
|
|
849
|
+
e = camelcaseKeys(eventData);
|
|
850
|
+
e.recordings = camelcaseKeys(eventData.recordings);
|
|
851
|
+
|
|
852
|
+
// Event all duration fields are in seconds, convert to millisecs
|
|
853
|
+
e.duration = e.duration * 1000;
|
|
854
|
+
e.answerDuration = e.answerDuration * 1000;
|
|
855
|
+
e.activeDuration = e.activeDuration * 1000;
|
|
856
|
+
|
|
857
|
+
// Add some extra convenience fields
|
|
858
|
+
e.durationStr = Tools.createDurationString(e.duration);
|
|
859
|
+
e.answerDurationStr = e.answerDuration ? Tools.createDurationString(e.answerDuration) : '';
|
|
860
|
+
e.activeDurationStr = e.activeDuration ? Tools.createDurationString(e.activeDuration) : '';
|
|
861
|
+
try {
|
|
862
|
+
this.emit('callDataInfo', e);
|
|
863
|
+
} catch (e) {
|
|
864
|
+
this.log.error('Exception in callDataInfo event: ' + e);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Check if we have media permission
|
|
869
|
+
_checkMediaPermission() {
|
|
870
|
+
// Check media is prepared
|
|
871
|
+
if (!this._mediaPermission) {
|
|
872
|
+
throw new Exceptions.MediaNotPrepared();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Session class created for every active call. You should use either call() or event incomingCall() to get new sessions
|
|
878
|
+
*
|
|
879
|
+
* @fires callEnded Receive basic information about call after call has been terminated
|
|
880
|
+
* @fires callDurationUpdate Regular updates on call duration (every second)
|
|
881
|
+
* @fires sessionStateChange Changes to session state
|
|
882
|
+
* @fires recordingStateChange When recording state changes
|
|
883
|
+
* @fires muteStateChange When mute state changes
|
|
884
|
+
* @fires holdStateChange When call is hold/unload
|
|
885
|
+
*
|
|
886
|
+
*/
|
|
887
|
+
export class Session extends EventEmitter {
|
|
888
|
+
constructor(phone) {
|
|
889
|
+
super();
|
|
890
|
+
|
|
891
|
+
// Parent Phone object
|
|
892
|
+
this._phone = phone;
|
|
893
|
+
|
|
894
|
+
// Use log instance from phone object
|
|
895
|
+
this.log = phone.log;
|
|
896
|
+
|
|
897
|
+
this._callUUID = null;
|
|
898
|
+
this._webRTCSession = null;
|
|
899
|
+
this._sessionState = null;
|
|
900
|
+
this._iceCandidateTimer = null;
|
|
901
|
+
this._isOnHold = false;
|
|
902
|
+
|
|
903
|
+
// Audio device flags
|
|
904
|
+
this._isMuted = false;
|
|
905
|
+
this._isRecording = false;
|
|
906
|
+
this._recordingError = null;
|
|
907
|
+
|
|
908
|
+
// Call timers
|
|
909
|
+
this._callBeginTime = null;
|
|
910
|
+
this._callAnswerTime = null;
|
|
911
|
+
this._callEndTime = null;
|
|
912
|
+
this._durationUpdTimerObj = null;
|
|
913
|
+
|
|
914
|
+
// Call information
|
|
915
|
+
this._originatingNumber = null;
|
|
916
|
+
this._destinationNumber = null;
|
|
917
|
+
this._callDisposition = null;
|
|
918
|
+
this._localNumberName = null;
|
|
919
|
+
this._isListenCall = false;
|
|
920
|
+
this._whisperMode = null;
|
|
921
|
+
this._manualDisposition = null;
|
|
922
|
+
|
|
923
|
+
// Quality report
|
|
924
|
+
this._rtcpStatsTimer = null;
|
|
925
|
+
this._currentReport = null;
|
|
926
|
+
|
|
927
|
+
// Timeout
|
|
928
|
+
this._answerCallTimeout = 0;
|
|
929
|
+
this._answerCallTimer = null;
|
|
930
|
+
|
|
931
|
+
// Current media switch handling the call
|
|
932
|
+
this._mediaSwitch = null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Create a new outbound session
|
|
936
|
+
newOutboundSession(number, callParams) {
|
|
937
|
+
// First create a UUID
|
|
938
|
+
this._callUUID = Tools.createUUID();
|
|
939
|
+
this.log.debug(`Created new outbound session to: ${number} [${this._callUUID}]`);
|
|
940
|
+
this.destinationNumber = number;
|
|
941
|
+
|
|
942
|
+
let options = {
|
|
943
|
+
mediaConstraints: { audio: true, video: false },
|
|
944
|
+
pcConfig: {
|
|
945
|
+
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
946
|
+
},
|
|
947
|
+
extraHeaders: [
|
|
948
|
+
'X-Call-token: ' + this._phone.options.callToken,
|
|
949
|
+
'X-P-UUID: ' + this._callUUID,
|
|
950
|
+
'X-Max-cost: ' + callParams.maximumCallCost,
|
|
951
|
+
],
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// Append explicit caller id if needed
|
|
955
|
+
if (callParams.explicitCallerId) {
|
|
956
|
+
options.extraHeaders.push('X-CID-Number-Id: ' + callParams.explicitCallerId);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Append explicit short cid if needed
|
|
960
|
+
if (callParams.explicitShortCallerId) {
|
|
961
|
+
options.extraHeaders.push('X-Short-CID-Id: ' + callParams.explicitShortCallerId);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Append meta info
|
|
965
|
+
if (callParams.metaInfo) {
|
|
966
|
+
options.extraHeaders.push('X-Meta-Info: ' + encodeURI(callParams.metaInfo.toString().substr(0, 255)));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Enable recording?
|
|
970
|
+
if (callParams.record) {
|
|
971
|
+
if (isNaN(callParams.record)) {
|
|
972
|
+
throw new Exceptions.InvalidParameter('Record parameter must be a number');
|
|
973
|
+
}
|
|
974
|
+
this.log.debug(`Recording enabled and kept for ${callParams.record} days`);
|
|
975
|
+
options.extraHeaders.push('X-Record: ' + callParams.record);
|
|
976
|
+
this._isRecording = true;
|
|
977
|
+
} else {
|
|
978
|
+
this._isRecording = false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Store listen flag
|
|
982
|
+
this._isListenCall = callParams._isListenCall;
|
|
983
|
+
|
|
984
|
+
// Manually specified media switch
|
|
985
|
+
if (callParams.mediaSwitchURI) {
|
|
986
|
+
this.log.debug('Requesting specific media switch ' + callParams.mediaSwitchURI);
|
|
987
|
+
options.extraHeaders.push('X-RMS: ' + callParams.mediaSwitchURI);
|
|
988
|
+
} else {
|
|
989
|
+
// Request specific media switch to group calls on same server
|
|
990
|
+
const otherSessions = this.getOtherSessions();
|
|
991
|
+
|
|
992
|
+
if (otherSessions.length > 0) {
|
|
993
|
+
if (otherSessions[0]._mediaSwitch) {
|
|
994
|
+
// There are existing active calls, append first media switch address found and request call grouping to this
|
|
995
|
+
this.log.debug('Requesting call grouping on media switch ' + otherSessions[0]._mediaSwitch);
|
|
996
|
+
options.extraHeaders.push('X-RMS: sip:' + otherSessions[0]._mediaSwitch);
|
|
997
|
+
} else {
|
|
998
|
+
this.log.warning('Other sessions exist but media switch address is invalid');
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
this.log.debug('No other active calls, no media switch grouping requested');
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Prepare invite uri
|
|
1006
|
+
const uri = 'sip:' + number + '@telegenta.com';
|
|
1007
|
+
|
|
1008
|
+
// Not start call and create sip session
|
|
1009
|
+
try {
|
|
1010
|
+
this._initWebRTCSession(this._phone._UA.call(uri, options));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
this.log.error('Error starting call:', error);
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (callParams.answerCallTimeout) {
|
|
1017
|
+
this._setAnswerCallTimeout(callParams.answerCallTimeout);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Create new inbound session
|
|
1022
|
+
newInboundSession(newSessionRequest) {
|
|
1023
|
+
this._callUUID = newSessionRequest.request.getHeader('X-P-UUID', 0);
|
|
1024
|
+
this._destinationNumber = newSessionRequest.request.getHeader('X-DID', 0);
|
|
1025
|
+
this._localNumberName = unescape(newSessionRequest.request.getHeader('X-DID-NAME', 0));
|
|
1026
|
+
this._originName = newSessionRequest.request.from.display_name;
|
|
1027
|
+
this._originatingNumber = newSessionRequest.request.from.uri.user;
|
|
1028
|
+
this.log.info(`Incoming call from ${this._originatingNumber} [${this._callUUID}]`);
|
|
1029
|
+
this.log.debug(newSessionRequest);
|
|
1030
|
+
|
|
1031
|
+
// Get media switch info
|
|
1032
|
+
this._mediaSwitch = newSessionRequest.request.getHeader('X-MS');
|
|
1033
|
+
this.log.debug(`Media switch handling call: ${this._mediaSwitch}`);
|
|
1034
|
+
|
|
1035
|
+
this._initWebRTCSession(newSessionRequest.session);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Return a list of all sessions except this session
|
|
1040
|
+
* Mostly used when transferring call
|
|
1041
|
+
*/
|
|
1042
|
+
getOtherSessions() {
|
|
1043
|
+
const allSessions = this._phone.sessions;
|
|
1044
|
+
|
|
1045
|
+
let s = [];
|
|
1046
|
+
for (let key in allSessions) {
|
|
1047
|
+
if (allSessions[key]) {
|
|
1048
|
+
if (allSessions[key].__containerId !== this.__containerId) {
|
|
1049
|
+
s.push(allSessions[key]);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return s;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Initialize with WebRTCSession object
|
|
1058
|
+
_initWebRTCSession(webRTCSession) {
|
|
1059
|
+
this._webRTCSession = webRTCSession;
|
|
1060
|
+
this._callBeginTime = Date.now();
|
|
1061
|
+
|
|
1062
|
+
// Hookup events
|
|
1063
|
+
const self = this;
|
|
1064
|
+
|
|
1065
|
+
// ICE CANDIDATES
|
|
1066
|
+
this._webRTCSession.on('icecandidate', function (event) {
|
|
1067
|
+
const c = event.candidate;
|
|
1068
|
+
self.log.debug(`Got ICE candidate: ${c.candidate}`);
|
|
1069
|
+
if (c.type === 'srflx' && c.relatedAddress !== null && c.relatedPort !== null) {
|
|
1070
|
+
self.log.info(`Accepting ICE candidate: ${c.candidate}`);
|
|
1071
|
+
|
|
1072
|
+
// Clear timer first
|
|
1073
|
+
if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
|
|
1074
|
+
|
|
1075
|
+
// Now fire ready
|
|
1076
|
+
event.ready();
|
|
1077
|
+
} else {
|
|
1078
|
+
// Start timeout
|
|
1079
|
+
if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
|
|
1080
|
+
|
|
1081
|
+
self._iceCandidateTimer = setTimeout(
|
|
1082
|
+
function (data) {
|
|
1083
|
+
self.log.debug(`ICE Timeout (${self._phone.options.iceGatherTimeout} ms) reached`);
|
|
1084
|
+
data.ready();
|
|
1085
|
+
},
|
|
1086
|
+
self._phone.options.iceGatherTimeout,
|
|
1087
|
+
event,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
this._webRTCSession.on('connecting', function (/* data */) {
|
|
1093
|
+
self._startDurationUpdateTimer();
|
|
1094
|
+
self._startRTCPStatsUpdateTimer();
|
|
1095
|
+
self._setSessionState('INITIALIZING');
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
this._webRTCSession.on('sending', function (/* data */) {
|
|
1099
|
+
// Play call initialized tone if needed
|
|
1100
|
+
if (self._phone.options.playCallInitializedTone) {
|
|
1101
|
+
self._phone._startRepeatTone(ST_CALLINIT);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
self._setSessionState('CALLING');
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
this._webRTCSession.on('progress', function (data) {
|
|
1108
|
+
if (data.originator === 'remote') {
|
|
1109
|
+
self._setSessionState('PROGRESS', { code: data.response.status_code });
|
|
1110
|
+
self._phone._endRepeatTone();
|
|
1111
|
+
|
|
1112
|
+
// Ringback handling on 180 RINGING
|
|
1113
|
+
if (data.response.status_code === 180) {
|
|
1114
|
+
self._phone._startRepeatTone(ST_RINGBACK);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Also start answer call timeout
|
|
1118
|
+
if (self._answerCallTimeout) {
|
|
1119
|
+
self._answerCallTimer = setTimeout(function () {
|
|
1120
|
+
self.log.info(`Answer call timeout after ${self._answerCallTimeout} seconds`);
|
|
1121
|
+
try {
|
|
1122
|
+
self.hangup();
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
self.log.info('No active session, ignoring answer call timeout');
|
|
1125
|
+
}
|
|
1126
|
+
}, self._answerCallTimeout * 1000);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
this._webRTCSession.on('accepted', function (data) {
|
|
1132
|
+
// Always end ringsback
|
|
1133
|
+
self._phone._endRepeatTone();
|
|
1134
|
+
|
|
1135
|
+
self._callAnswerTime = Date.now();
|
|
1136
|
+
|
|
1137
|
+
// Call was answered, clear answer call timeout timer if needed
|
|
1138
|
+
if (self._answerCallTimeout) {
|
|
1139
|
+
clearTimeout(self._answerCallTimer);
|
|
1140
|
+
self._answerCallTimeout = 0;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Get initial recording state
|
|
1144
|
+
if (data.originator === 'remote') {
|
|
1145
|
+
const recState = data.response.getHeader('X-Recording');
|
|
1146
|
+
if (recState === 'true') {
|
|
1147
|
+
self.log.debug('Call is being prerecorded');
|
|
1148
|
+
// self._isRecording = true;
|
|
1149
|
+
// self.emit('recordingStateChange', {state: true, error: null});
|
|
1150
|
+
} else {
|
|
1151
|
+
self.log.debug('No call recording started yet');
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Get media switch info
|
|
1155
|
+
self._mediaSwitch = data.response.getHeader('X-MS');
|
|
1156
|
+
self.log.debug(`Media switch handling call: ${self._mediaSwitch}`);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
this._webRTCSession.on('confirmed', function (/* data */) {
|
|
1161
|
+
self._setSessionState('ACTIVE');
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
this._webRTCSession.on('ended', function (/* data */) {
|
|
1165
|
+
// End any ringsbacks if needed
|
|
1166
|
+
self._phone._endRepeatTone();
|
|
1167
|
+
|
|
1168
|
+
self._callEndTime = Date.now();
|
|
1169
|
+
|
|
1170
|
+
// Clear answer call timeout timer if needed
|
|
1171
|
+
if (self._answerCallTimeout) {
|
|
1172
|
+
clearTimeout(self._answerCallTimer);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
self._setSessionState('TERMINATED');
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
this._webRTCSession.on('failed', function (data) {
|
|
1179
|
+
// End any ringsbacks if needed
|
|
1180
|
+
self._phone._endRepeatTone();
|
|
1181
|
+
|
|
1182
|
+
// self.log.error(data);
|
|
1183
|
+
self._callEndTime = Date.now();
|
|
1184
|
+
|
|
1185
|
+
// Clear answer call timeout timer if needed
|
|
1186
|
+
if (self._answerCallTimeout) {
|
|
1187
|
+
clearTimeout(self._answerCallTimer);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let m;
|
|
1191
|
+
|
|
1192
|
+
// Append message if available
|
|
1193
|
+
if (data.message) {
|
|
1194
|
+
m = `${data.message.status_code} ${data.message.reason_phrase}`;
|
|
1195
|
+
} else {
|
|
1196
|
+
m = '';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
self._setSessionState('FAILED', { cause: data.cause, message: m });
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
this._webRTCSession.on('dtmf', function (data) {
|
|
1203
|
+
self.log.debug('Session DTMF', data);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
this._webRTCSession.on('newInfo', function (data) {
|
|
1207
|
+
self.log.debug('New session info', data);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
this._webRTCSession.on('hold', function (/* data */) {
|
|
1211
|
+
self._isOnHold = true;
|
|
1212
|
+
self._fireHoldStateEvent();
|
|
1213
|
+
self._setSessionState('ON_HOLD');
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
this._webRTCSession.on('unhold', function (/* data */) {
|
|
1217
|
+
self._isOnHold = false;
|
|
1218
|
+
self._fireHoldStateEvent();
|
|
1219
|
+
self._setSessionState('ACTIVE');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
this._webRTCSession.on('muted', function (data) {
|
|
1223
|
+
self.log.debug('Microphone muted', data);
|
|
1224
|
+
self._isMuted = true;
|
|
1225
|
+
self._fireMuteStateEvent();
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
this._webRTCSession.on('unmuted', function (data) {
|
|
1229
|
+
self.log.debug('Microphone unmuted', data);
|
|
1230
|
+
self._isMuted = false;
|
|
1231
|
+
self._fireMuteStateEvent();
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
this._webRTCSession.on('reinvite', function (data) {
|
|
1235
|
+
self.log.debug('Session reinvite', data);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
this._webRTCSession.on('update', function (data) {
|
|
1239
|
+
self.log.debug('Session update', data);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
this._webRTCSession.on('getusermediafailed', function (data) {
|
|
1243
|
+
self.log.error('Getusermedia failed', data);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// If connection connect audio stream
|
|
1247
|
+
if (self._webRTCSession.connection) {
|
|
1248
|
+
self._handleRemoteAudio(self._webRTCSession);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// DEBUG STUFF
|
|
1252
|
+
this.log.debug('WebRTCSession object:');
|
|
1253
|
+
this.log.debug(this);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Hangup call
|
|
1258
|
+
* @param {CALL_DISPOSITION} disposition - Hangup call with specific disposition
|
|
1259
|
+
* @throws NoActiveSession - If no active session / call exists
|
|
1260
|
+
*/
|
|
1261
|
+
hangup(disposition = null) {
|
|
1262
|
+
if (!disposition) {
|
|
1263
|
+
this.log.info('Hanging up call');
|
|
1264
|
+
this._webRTCSession.terminate();
|
|
1265
|
+
} else {
|
|
1266
|
+
this.log.info(`Hanging up call with disposition ${disposition}`);
|
|
1267
|
+
this._manualDisposition = disposition;
|
|
1268
|
+
this._webRTCSession.terminate({
|
|
1269
|
+
extraHeaders: ['X-HDISP: ' + disposition],
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Answer incoming call
|
|
1276
|
+
* @param metaInfo - Add meta info to this call
|
|
1277
|
+
*/
|
|
1278
|
+
answer(metaInfo = null) {
|
|
1279
|
+
let options = {
|
|
1280
|
+
mediaConstraints: { audio: true, video: false },
|
|
1281
|
+
pcConfig: {
|
|
1282
|
+
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
1283
|
+
},
|
|
1284
|
+
extraHeaders: ['X-Call-token: ' + this._phone.options.callToken],
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// Append meta info if needed
|
|
1288
|
+
if (metaInfo) {
|
|
1289
|
+
options.extraHeaders.push('X-Meta-Info: ' + encodeURI(metaInfo.toString().substr(0, 255)));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
this._webRTCSession.answer(options);
|
|
1293
|
+
|
|
1294
|
+
// After answer we start remote audio
|
|
1295
|
+
this._handleRemoteAudio(this._webRTCSession);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Reject incoming call
|
|
1300
|
+
* @param statusCode - SIP Status code
|
|
1301
|
+
* @param reasonPhrase - Reason phrase
|
|
1302
|
+
*/
|
|
1303
|
+
reject(statusCode = 603, reasonPhrase = 'Rejected call') {
|
|
1304
|
+
this.log.info('Rejecting call');
|
|
1305
|
+
this._webRTCSession.terminate({
|
|
1306
|
+
status_code: statusCode,
|
|
1307
|
+
reason_phrase: reasonPhrase,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Return UUID of call
|
|
1313
|
+
* @returns {string} UUID of call
|
|
1314
|
+
*/
|
|
1315
|
+
get uuid() {
|
|
1316
|
+
return this._callUUID;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Destination number called
|
|
1321
|
+
* @returns {string} called number
|
|
1322
|
+
*/
|
|
1323
|
+
get destinationNumber() {
|
|
1324
|
+
return this._destinationNumber;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Name of local number (for inbound)
|
|
1329
|
+
* @returns {string} Name of local number called
|
|
1330
|
+
*/
|
|
1331
|
+
get localNumberName() {
|
|
1332
|
+
return this._localNumberName;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Originating number
|
|
1337
|
+
* @returns {string} Number of calling party
|
|
1338
|
+
*/
|
|
1339
|
+
get originatingNumber() {
|
|
1340
|
+
return this._originatingNumber;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Set destination number
|
|
1344
|
+
set destinationNumber(number) {
|
|
1345
|
+
this.log.debug('Setting destination number to: ' + number);
|
|
1346
|
+
this._destinationNumber = number;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Session state
|
|
1351
|
+
* @returns {SESSION_STATE} current session state
|
|
1352
|
+
*/
|
|
1353
|
+
get state() {
|
|
1354
|
+
return this._sessionState;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Session call direction
|
|
1359
|
+
* @returns {CALL_DIRECTION} current session state
|
|
1360
|
+
*/
|
|
1361
|
+
get direction() {
|
|
1362
|
+
if (this._isListenCall) {
|
|
1363
|
+
this.log.debug('Call direction is LISTEN');
|
|
1364
|
+
return 'LISTEN';
|
|
1365
|
+
} else if (this._webRTCSession.direction === 'incoming') {
|
|
1366
|
+
this.log.debug('Call direction is INBOUND');
|
|
1367
|
+
return 'INBOUND';
|
|
1368
|
+
} else if (this._webRTCSession.direction === 'outgoing') {
|
|
1369
|
+
this.log.debug('Call direction is OUTBOUND');
|
|
1370
|
+
return 'OUTBOUND';
|
|
1371
|
+
} else {
|
|
1372
|
+
this.log.error('Unknown call direction: ' + this._webRTCSession.direction);
|
|
1373
|
+
return 'UNKNOWN';
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// RECORDING
|
|
1378
|
+
/**
|
|
1379
|
+
* Start recording
|
|
1380
|
+
*/
|
|
1381
|
+
startRecording() {
|
|
1382
|
+
const opt = {
|
|
1383
|
+
extraHeaders: ['Record: on'],
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
this.log.debug('Request start recording');
|
|
1387
|
+
|
|
1388
|
+
const self = this;
|
|
1389
|
+
this._webRTCSession.once('newInfo', (data) => {
|
|
1390
|
+
if (data.originator === 'local') {
|
|
1391
|
+
data.info.on('succeeded', function (/* data */) {
|
|
1392
|
+
self.log.info('Recording started successfully');
|
|
1393
|
+
self._isRecording = true;
|
|
1394
|
+
try {
|
|
1395
|
+
self.emit('recordingStateChange', { state: true, error: null });
|
|
1396
|
+
} catch (e) {
|
|
1397
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
data.info.on('failed', function (data) {
|
|
1402
|
+
self._recordingError = data.response.reason_phrase;
|
|
1403
|
+
self.log.error('Recording start failed: ' + self._recordingError);
|
|
1404
|
+
self._fireRecordingStateEvent();
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
this._webRTCSession.sendInfo('application/info', null, opt);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Stop recording
|
|
1414
|
+
*/
|
|
1415
|
+
stopRecording() {
|
|
1416
|
+
const opt = {
|
|
1417
|
+
extraHeaders: ['Record: off'],
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
this.log.debug('Request stop recording');
|
|
1421
|
+
|
|
1422
|
+
const self = this;
|
|
1423
|
+
|
|
1424
|
+
this._webRTCSession.once('newInfo', (data) => {
|
|
1425
|
+
data.info.on('succeeded', function (/* data */) {
|
|
1426
|
+
self.log.info('Recording stopped successfully');
|
|
1427
|
+
self._isRecording = false;
|
|
1428
|
+
try {
|
|
1429
|
+
self.emit('recordingStateChange', { state: false, error: null });
|
|
1430
|
+
} catch (e) {
|
|
1431
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
data.info.on('failed', function (data) {
|
|
1436
|
+
self.log.error('Recording stopped failed: ' + data.response.reason_phrase);
|
|
1437
|
+
try {
|
|
1438
|
+
self.emit('recordingStateChange', { state: self._isRecording, error: data.response.reason_phrase });
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
self.log.error('Exception in recordingStateChange event: ' + e);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
this._webRTCSession.sendInfo('application/info', null, opt);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Toogle recording ON/OFF
|
|
1450
|
+
*/
|
|
1451
|
+
toggleRecording() {
|
|
1452
|
+
if (!this._isRecording) {
|
|
1453
|
+
this.startRecording();
|
|
1454
|
+
return true;
|
|
1455
|
+
} else {
|
|
1456
|
+
this.stopRecording();
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Return recording state
|
|
1463
|
+
* @return {boolean}
|
|
1464
|
+
*/
|
|
1465
|
+
get isRecording() {
|
|
1466
|
+
return this._isRecording;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// MUTE
|
|
1470
|
+
/**
|
|
1471
|
+
* Mute microphone
|
|
1472
|
+
*/
|
|
1473
|
+
muteMic() {
|
|
1474
|
+
this.log.info('Mute microphone');
|
|
1475
|
+
this._webRTCSession.mute();
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Unmute microphone
|
|
1480
|
+
*/
|
|
1481
|
+
unmuteMic() {
|
|
1482
|
+
this.log.info('Unmute microphone');
|
|
1483
|
+
this._webRTCSession.unmute();
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Toggle microphone mute state
|
|
1488
|
+
*/
|
|
1489
|
+
toggleMute() {
|
|
1490
|
+
if (!this._isMuted) {
|
|
1491
|
+
this.muteMic();
|
|
1492
|
+
} else {
|
|
1493
|
+
this.unmuteMic();
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Return mute state
|
|
1499
|
+
* @return {bool}
|
|
1500
|
+
*/
|
|
1501
|
+
get isMuted() {
|
|
1502
|
+
return this._isMuted;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// HOLD
|
|
1506
|
+
/**
|
|
1507
|
+
* Hold call
|
|
1508
|
+
*/
|
|
1509
|
+
hold() {
|
|
1510
|
+
this.log.info('Hold call');
|
|
1511
|
+
this._webRTCSession.hold();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Unhold call
|
|
1516
|
+
*/
|
|
1517
|
+
unhold() {
|
|
1518
|
+
this.log.info('Uncall call');
|
|
1519
|
+
this._phone.holdAllCalls();
|
|
1520
|
+
this._webRTCSession.unhold();
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Toggle call hold
|
|
1525
|
+
*/
|
|
1526
|
+
toggleHold() {
|
|
1527
|
+
if (!this._isOnHold) {
|
|
1528
|
+
this.hold();
|
|
1529
|
+
} else {
|
|
1530
|
+
this.unhold();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Return call hold status
|
|
1536
|
+
* @return {bool}
|
|
1537
|
+
*/
|
|
1538
|
+
get isOnHold() {
|
|
1539
|
+
return this._isOnHold;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Send DTMF tone on call
|
|
1544
|
+
* @param code What DTMF code to send (valid: 0-9,*,#,d)
|
|
1545
|
+
* @param playLocal=True Play DTMF sound locally also
|
|
1546
|
+
* @throws InvalidParameter - On incorrect DTMF codes
|
|
1547
|
+
*/
|
|
1548
|
+
sendDTMF(code, playLocal = true) {
|
|
1549
|
+
// Check for correct DTMF and get frequencies
|
|
1550
|
+
if (!(code in DTMF_FREQUENCY_TABLE)) {
|
|
1551
|
+
throw new Exceptions.InvalidParameter('Invalid DTMF code');
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
this.log.info('Sending DTMF: ' + code);
|
|
1555
|
+
this._webRTCSession.sendDTMF(code);
|
|
1556
|
+
|
|
1557
|
+
if (playLocal) {
|
|
1558
|
+
const f = DTMF_FREQUENCY_TABLE[code];
|
|
1559
|
+
|
|
1560
|
+
// First mute microphone before playing DTMF
|
|
1561
|
+
if (this._isMuted) {
|
|
1562
|
+
this.log.debug('Mic already muted, just sending DTMF');
|
|
1563
|
+
this._phone.playTone(500, f[0], f[1]);
|
|
1564
|
+
} else {
|
|
1565
|
+
this.log.debug('Mute mic while sending DTMF');
|
|
1566
|
+
this._webRTCSession.mute();
|
|
1567
|
+
|
|
1568
|
+
const self = this;
|
|
1569
|
+
this._phone.playTone(500, f[0], f[1], function () {
|
|
1570
|
+
self.log.debug('Unmute mic after DTMF');
|
|
1571
|
+
self._webRTCSession.unmute();
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Transfer call to another session (attended transfer)
|
|
1579
|
+
*
|
|
1580
|
+
* @param dstSession Destination session
|
|
1581
|
+
*/
|
|
1582
|
+
transfer(dstSession) {
|
|
1583
|
+
this.log.info(`Transferring ${this.uuid} => ${dstSession.uuid}`);
|
|
1584
|
+
|
|
1585
|
+
// Create ReferSubscriber event class
|
|
1586
|
+
const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, {
|
|
1587
|
+
replaces: dstSession._webRTCSession,
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
const self = this;
|
|
1591
|
+
|
|
1592
|
+
// Setup some logging
|
|
1593
|
+
referSubscriber.on('requestSucceeded', function () {
|
|
1594
|
+
self.log.info('Transfer completed successfully');
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
referSubscriber.on('requestFailed', function (cause) {
|
|
1598
|
+
self.log.error(`Transfer rejected: ${cause}`);
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
referSubscriber.on('failed', function (cause) {
|
|
1602
|
+
self.log.error(`Transfer failed: ${cause}`);
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Playback media file into call. You must be connected the event service to use this feature (set connectEventService=true)
|
|
1608
|
+
*
|
|
1609
|
+
* @param media_file_id Media file to playback
|
|
1610
|
+
* @param hangup_after Hangup call after playback ends
|
|
1611
|
+
*
|
|
1612
|
+
* @throws NotAllowed - If event service is not enabled
|
|
1613
|
+
*
|
|
1614
|
+
* @return Promise that resolves when playback has started
|
|
1615
|
+
*/
|
|
1616
|
+
playback(media_file_id, hangup_after = false) {
|
|
1617
|
+
// Check event service is enabled first
|
|
1618
|
+
if (!this._phone._options.connectEventService) {
|
|
1619
|
+
throw new Exceptions.NotAllowed('Event service not enabled');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
this.log.info(`Playback media file ${media_file_id} hangup after ${hangup_after}`);
|
|
1623
|
+
|
|
1624
|
+
const p = this._phone._eventClient.fireEvent('call_command', {
|
|
1625
|
+
call_uuid: this.uuid,
|
|
1626
|
+
call_command: 'playback',
|
|
1627
|
+
media_file_id: media_file_id,
|
|
1628
|
+
hangup_after: hangup_after,
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// When playback is acknowledged and 'hangup_after' is true, we set session state into PLAYBACK
|
|
1632
|
+
if (hangup_after) {
|
|
1633
|
+
// Stop remote audio
|
|
1634
|
+
this._phone._remoteAudioElement.srcObject = null;
|
|
1635
|
+
|
|
1636
|
+
// Mute microphone
|
|
1637
|
+
this._webRTCSession.mute();
|
|
1638
|
+
|
|
1639
|
+
// When playback starts set state to playback
|
|
1640
|
+
p.then(() => {
|
|
1641
|
+
// Set session state
|
|
1642
|
+
this._setSessionState('PLAYBACK');
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return p;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Set whisper mode on "listen" calls
|
|
1651
|
+
*
|
|
1652
|
+
* @param mode Whisper mode 0 = No whisper / 1 = local peer only / 2 = Three way (both local and remote)
|
|
1653
|
+
*
|
|
1654
|
+
* @throws NotAllowed - If call is not listen call
|
|
1655
|
+
* @throws InvalidOptions - If invalid mode given
|
|
1656
|
+
*/
|
|
1657
|
+
setWhisperMode(mode = 0) {
|
|
1658
|
+
if (!this.direction === 'LISTEN') {
|
|
1659
|
+
throw new Exceptions.NotAllowed('Call is not correct type (listen)');
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (mode < 0 || mode > 2) {
|
|
1663
|
+
throw new Exceptions.InvalidOptions('Invalid mode');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
let t_dtmf_mode;
|
|
1667
|
+
|
|
1668
|
+
if (mode === 0) {
|
|
1669
|
+
t_dtmf_mode = 0;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (mode === 1) {
|
|
1673
|
+
t_dtmf_mode = 2;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (mode === 2) {
|
|
1677
|
+
t_dtmf_mode = 3;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
this.sendDTMF(t_dtmf_mode, false);
|
|
1681
|
+
this._whisperMode = mode;
|
|
1682
|
+
this._fireWhisperModeChangeEvent();
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/*
|
|
1686
|
+
* ======= PRIVATE METHODS PHONE SESSION =========================================================================
|
|
1687
|
+
*/
|
|
1688
|
+
|
|
1689
|
+
// Begin update timer events
|
|
1690
|
+
_startDurationUpdateTimer() {
|
|
1691
|
+
/**
|
|
1692
|
+
* @event callDurationUpdate
|
|
1693
|
+
* @type {object}
|
|
1694
|
+
* @property {String} uuid - UUID of session
|
|
1695
|
+
* @property {Date} beginTime - Time session was created
|
|
1696
|
+
* @property {Date} answerTime - Time session was answered
|
|
1697
|
+
* @property {Date} duration - The total duration of the session
|
|
1698
|
+
* @property {String} durationStr - The duration of the session string formated (mm:ss)
|
|
1699
|
+
* @property {Date} activeDuration - The active duration of the session
|
|
1700
|
+
* @property {String} activeDurationStr - The active duration of the session string formated (mm:ss)
|
|
1701
|
+
*
|
|
1702
|
+
* @example
|
|
1703
|
+
* // Update duration text on screen
|
|
1704
|
+
* session.on('callDurationUpdate', function (event) {
|
|
1705
|
+
* document.getElementById('duration').textContent = event.durationStr;
|
|
1706
|
+
* });
|
|
1707
|
+
*/
|
|
1708
|
+
|
|
1709
|
+
const self = this;
|
|
1710
|
+
if (!this._durationUpdTimerObj) {
|
|
1711
|
+
this._durationUpdTimerObj = setInterval(function () {
|
|
1712
|
+
const activeDuration = self._callAnswerTime ? Date.now() - self._callAnswerTime : null;
|
|
1713
|
+
const e = {
|
|
1714
|
+
uuid: self._callUUID,
|
|
1715
|
+
beginTime: self._callBeginTime,
|
|
1716
|
+
answerTime: self._callAnswerTime,
|
|
1717
|
+
endTime: self._callEndTime,
|
|
1718
|
+
duration: Date.now() - self._callBeginTime,
|
|
1719
|
+
durationStr: Tools.createDurationString(Date.now() - self._callBeginTime),
|
|
1720
|
+
activeDuration: activeDuration,
|
|
1721
|
+
activeDurationStr: activeDuration ? Tools.createDurationString(activeDuration) : '',
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
try {
|
|
1725
|
+
self.emit('callDurationUpdate', e);
|
|
1726
|
+
} catch (e) {
|
|
1727
|
+
self.log.error('Exception in call duration update event: ' + e);
|
|
1728
|
+
}
|
|
1729
|
+
}, 1000);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Begin rtcp stats update timer events
|
|
1734
|
+
_startRTCPStatsUpdateTimer() {
|
|
1735
|
+
/**
|
|
1736
|
+
* @event callQualityReportUpdate
|
|
1737
|
+
* @description
|
|
1738
|
+
* Contains information about call quality (fires in interval of 5 sec).
|
|
1739
|
+
* Payload contains "total" (metrics for whole session) and "delta" (metrics last 5 secs).
|
|
1740
|
+
*
|
|
1741
|
+
* @type {object}
|
|
1742
|
+
* @property {String} uuid - UUID of session
|
|
1743
|
+
* @property {Number} outboundPacketsSent - Number of packets sent
|
|
1744
|
+
* @property {Number} outboundBytesSent - Number of bytes sent
|
|
1745
|
+
* @property {Number} outboundPacketsLost - Number of packets lost to remote
|
|
1746
|
+
* @property {Number} outboundPacketLossPct - Percentage of outbound packets lost
|
|
1747
|
+
* @property {Number} outboundJitter - Network jitter to remote (ms)
|
|
1748
|
+
* @property {Number} inboundPacketsReceived - Number of packets received
|
|
1749
|
+
* @property {Number} inboundBytesReceived - Number of bytes received
|
|
1750
|
+
* @property {Number} inboundPacketsLost - Number of packets lost from remote
|
|
1751
|
+
* @property {Number} inboundPacketLossPct - Percentage of inbound packets lost
|
|
1752
|
+
* @property {Number} inboundJitter - Network jitter from remote (ms)
|
|
1753
|
+
* @property {Number} roundTripTime - Latency in ms to media gateway
|
|
1754
|
+
* @property {Number} effectiveLatency - Effective latency with combined jitter impact
|
|
1755
|
+
* @property {Number} rFactor - Quality of media stream (0 - 100%)
|
|
1756
|
+
* @property {Number} mos - Synthetic MOS
|
|
1757
|
+
* @property {String} qualityString - Quality string [Excellent, Good, Fair, Poor, Bad]
|
|
1758
|
+
*
|
|
1759
|
+
* @example
|
|
1760
|
+
* // Update call quality text on screen
|
|
1761
|
+
* session.on('callQualityReportUpdate', (report) => {
|
|
1762
|
+
* document.getElementById('quality_text').textContent = report.delta.qualityString;
|
|
1763
|
+
* document.getElementById('latency').textContent = report.delta.effectiveLatency;
|
|
1764
|
+
* });
|
|
1765
|
+
*/
|
|
1766
|
+
|
|
1767
|
+
if (!this._rtcpStatsTimer) {
|
|
1768
|
+
const emptyReport = {
|
|
1769
|
+
outboundPacketsSent: 0,
|
|
1770
|
+
outboundBytesSent: 0,
|
|
1771
|
+
outboundPacketsLost: 0,
|
|
1772
|
+
outboundJitter: 0,
|
|
1773
|
+
inboundPacketsReceived: 0,
|
|
1774
|
+
inboundBytesReceived: 0,
|
|
1775
|
+
inboundPacketsLost: 0,
|
|
1776
|
+
inboundJitter: 0,
|
|
1777
|
+
roundTripTime: 0,
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
// Initialize report first
|
|
1781
|
+
this._currentReport = Object.assign({}, emptyReport);
|
|
1782
|
+
|
|
1783
|
+
this._rtcpStatsTimer = setInterval(() => {
|
|
1784
|
+
this._webRTCSession._connection.getStats().then((data) => {
|
|
1785
|
+
let totalReport = Object.assign({}, emptyReport);
|
|
1786
|
+
let deltaReport = Object.assign({}, emptyReport);
|
|
1787
|
+
|
|
1788
|
+
for (let report of data.values()) {
|
|
1789
|
+
// this.log.debug(report);
|
|
1790
|
+
switch (report.type) {
|
|
1791
|
+
case 'outbound-rtp':
|
|
1792
|
+
// this.log.debug('Got: outbound-rtp');
|
|
1793
|
+
// this.log.debug(report);
|
|
1794
|
+
// Packets
|
|
1795
|
+
totalReport.outboundPacketsSent = report.packetsSent;
|
|
1796
|
+
deltaReport.outboundPacketsSent =
|
|
1797
|
+
report.packetsSent - this._currentReport.outboundPacketsSent;
|
|
1798
|
+
// Bytes
|
|
1799
|
+
totalReport.outboundBytesSent = report.bytesSent;
|
|
1800
|
+
deltaReport.outboundBytesSent =
|
|
1801
|
+
report.bytesSent - this._currentReport.outboundBytesSent;
|
|
1802
|
+
break;
|
|
1803
|
+
|
|
1804
|
+
case 'inbound-rtp':
|
|
1805
|
+
// this.log.debug('Got: inbound-rtp');
|
|
1806
|
+
// this.log.debug(report);
|
|
1807
|
+
// Packets
|
|
1808
|
+
totalReport.inboundPacketsReceived = report.packetsReceived;
|
|
1809
|
+
deltaReport.inboundPacketsReceived =
|
|
1810
|
+
report.packetsReceived - this._currentReport.inboundPacketsReceived;
|
|
1811
|
+
// Bytes
|
|
1812
|
+
totalReport.inboundBytesReceived = report.bytesReceived;
|
|
1813
|
+
deltaReport.inboundBytesReceived =
|
|
1814
|
+
report.bytesReceived - this._currentReport.inboundBytesReceived;
|
|
1815
|
+
// Loss
|
|
1816
|
+
totalReport.inboundPacketsLost = report.packetsLost;
|
|
1817
|
+
deltaReport.inboundPacketsLost =
|
|
1818
|
+
report.packetsLost - this._currentReport.inboundPacketsLost;
|
|
1819
|
+
// Jitter
|
|
1820
|
+
totalReport.inboundJitter = report.jitter * 1000;
|
|
1821
|
+
deltaReport.inboundJitter = report.jitter * 1000;
|
|
1822
|
+
break;
|
|
1823
|
+
|
|
1824
|
+
case 'remote-inbound-rtp':
|
|
1825
|
+
// this.log.debug('Got: remote-inbound-rtp');
|
|
1826
|
+
// this.log.debug(report);
|
|
1827
|
+
// Loss
|
|
1828
|
+
totalReport.outboundPacketsLost = report.packetsLost;
|
|
1829
|
+
deltaReport.outboundPacketsLost =
|
|
1830
|
+
report.packetsLost - this._currentReport.outboundPacketsLost;
|
|
1831
|
+
// Jitter
|
|
1832
|
+
totalReport.outboundJitter = report.jitter * 1000;
|
|
1833
|
+
deltaReport.outboundJitter = report.jitter * 1000;
|
|
1834
|
+
// RTT
|
|
1835
|
+
totalReport.roundTripTime = report.roundTripTime * 1000;
|
|
1836
|
+
deltaReport.roundTripTime = report.roundTripTime * 1000;
|
|
1837
|
+
break;
|
|
1838
|
+
|
|
1839
|
+
default:
|
|
1840
|
+
// this.log.debug(`Report ${report.type} ignored`);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
this._currentReport = this._calcReportMetrics(totalReport);
|
|
1846
|
+
|
|
1847
|
+
// We have to wait for RTT before firing the first event
|
|
1848
|
+
if (totalReport.roundTripTime) {
|
|
1849
|
+
// Do current report metrics
|
|
1850
|
+
const finalReport = {
|
|
1851
|
+
total: this._currentReport,
|
|
1852
|
+
delta: this._calcReportMetrics(deltaReport),
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
try {
|
|
1856
|
+
this.emit('callQualityReportUpdate', finalReport);
|
|
1857
|
+
} catch (e) {
|
|
1858
|
+
this.log.error('Exception in call quality report update event: ' + e);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
}, 5000);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Return quality string based on R-factor
|
|
1867
|
+
_calcReportMetrics(report) {
|
|
1868
|
+
// Packet loss
|
|
1869
|
+
report.outboundPacketLossPct = report.outboundPacketsLost / report.outboundPacketsSent;
|
|
1870
|
+
report.inboundPacketLossPct = report.inboundPacketsLost / report.inboundPacketsReceived;
|
|
1871
|
+
|
|
1872
|
+
// Effective latency
|
|
1873
|
+
report.effectiveLatency = report.roundTripTime + (report.inboundJitter + report.outboundJitter) + 10;
|
|
1874
|
+
|
|
1875
|
+
// R-Factor and mos
|
|
1876
|
+
const lossRate = (report.outboundPacketLossPct + report.inboundPacketLossPct) * 100;
|
|
1877
|
+
const lossEffect = (95.0 * lossRate) / (lossRate + 10);
|
|
1878
|
+
const latencyEffect = report.effectiveLatency * 0.1;
|
|
1879
|
+
// this.log.debug(`R factor loss effect ${lossEffect} / latency effect ${latencyEffect}`);
|
|
1880
|
+
|
|
1881
|
+
report.rFactor = Math.max(0, 100 - lossEffect - latencyEffect);
|
|
1882
|
+
report.mos =
|
|
1883
|
+
1 + 0.035 * report.rFactor + 0.000007 * report.rFactor * (report.rFactor - 60) * (100 - report.rFactor);
|
|
1884
|
+
|
|
1885
|
+
// Quality string
|
|
1886
|
+
switch (true) {
|
|
1887
|
+
case report.rFactor < 50:
|
|
1888
|
+
report.qualityString = 'Bad';
|
|
1889
|
+
break;
|
|
1890
|
+
case report.rFactor < 70:
|
|
1891
|
+
report.qualityString = 'Poor';
|
|
1892
|
+
break;
|
|
1893
|
+
case report.rFactor < 80:
|
|
1894
|
+
report.qualityString = 'Fair';
|
|
1895
|
+
break;
|
|
1896
|
+
case report.rFactor < 90:
|
|
1897
|
+
report.qualityString = 'Good';
|
|
1898
|
+
break;
|
|
1899
|
+
case report.rFactor <= 100:
|
|
1900
|
+
report.qualityString = 'Excellent';
|
|
1901
|
+
break;
|
|
1902
|
+
default:
|
|
1903
|
+
report.qualityString = 'Error';
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return report;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* @typedef SESSION_STATE {string}
|
|
1911
|
+
* @property {string} INITIALIZING Session is initializing (Gather ICE candidates)
|
|
1912
|
+
* @property {string} CALLING Sending INVITE to server
|
|
1913
|
+
* @property {string} PROGRESS Call progress received ('event.code' has new progress status)
|
|
1914
|
+
* @property {string} ACTIVE Call is active (answered)
|
|
1915
|
+
* @property {string} TERMINATED Call ended
|
|
1916
|
+
* @property {string} FAILED Call failed (see 'event.cause' for more info)
|
|
1917
|
+
* @property {string} ON_HOLD Call is on-hold
|
|
1918
|
+
* @property {string} INCOMING Incoming call is waiting
|
|
1919
|
+
* @property {string} PLAYBACK Playback with pending hangup in progress
|
|
1920
|
+
*/
|
|
1921
|
+
|
|
1922
|
+
// Set session state and emit event
|
|
1923
|
+
_setSessionState(state, info = null) {
|
|
1924
|
+
/**
|
|
1925
|
+
* @event sessionStateChange
|
|
1926
|
+
* @type {object}
|
|
1927
|
+
* @param {SESSION_STATE} state - The current session state
|
|
1928
|
+
* @param {uuid} uuid - Call UUID for session
|
|
1929
|
+
* @param {string} originName - Originating name of call [INCOMING only]
|
|
1930
|
+
* @param {string} originatingNumber - Originating phone number of call [INCOMING only]
|
|
1931
|
+
* @param {string} destinationNumber - Destination number of call
|
|
1932
|
+
* @param {string} cause - Fail cause [FAILED only]
|
|
1933
|
+
* @param {string} message - Fail message description [FAILED only]
|
|
1934
|
+
*
|
|
1935
|
+
*/
|
|
1936
|
+
|
|
1937
|
+
this.log.debug('Session state: ' + state);
|
|
1938
|
+
if (info) {
|
|
1939
|
+
this.log.debug(info);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
this._sessionState = state;
|
|
1943
|
+
try {
|
|
1944
|
+
this.emit(
|
|
1945
|
+
'sessionStateChange',
|
|
1946
|
+
Object.assign(
|
|
1947
|
+
{
|
|
1948
|
+
state: state,
|
|
1949
|
+
uuid: this._callUUID,
|
|
1950
|
+
},
|
|
1951
|
+
info,
|
|
1952
|
+
),
|
|
1953
|
+
);
|
|
1954
|
+
} catch (e) {
|
|
1955
|
+
this.log.error('Exception in sessionStateChange event:' + e);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Trigger callDataAvailable dummy event
|
|
1959
|
+
if (state === 'TERMINATED') {
|
|
1960
|
+
this._callDisposition = this._manualDisposition || 'NORMAL';
|
|
1961
|
+
this._callFinished();
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (state === 'FAILED') {
|
|
1965
|
+
if (info.cause === 'Unavailable') {
|
|
1966
|
+
this._callDisposition = 'TEMP_UNAVAIL';
|
|
1967
|
+
if (this._phone.options.playSignalTones) {
|
|
1968
|
+
this._phone._startRepeatTone(ST_TEMP_UNAVAIL);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
if (info.cause === 'Not Found') {
|
|
1973
|
+
this._callDisposition = 'INVALID_NUMBER';
|
|
1974
|
+
if (this._phone.options.playSignalTones) {
|
|
1975
|
+
this._phone._startRepeatTone(ST_INVALID_NUMBER);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if (info.cause === 'Canceled') {
|
|
1980
|
+
this._callDisposition = this._manualDisposition || 'NO_ANSWER';
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (info.cause === 'Rejected') {
|
|
1984
|
+
if (this.direction === 'INBOUND') {
|
|
1985
|
+
this._callDisposition = 'REJECTED';
|
|
1986
|
+
} else {
|
|
1987
|
+
this._callDisposition = 'BARRED';
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (info.cause === 'Busy') {
|
|
1992
|
+
this._callDisposition = 'BUSY';
|
|
1993
|
+
if (this._phone.options.playSignalTones) {
|
|
1994
|
+
this._phone._startRepeatTone(ST_BUSY);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// If session is incoming and failed, we don't fire calldataAvailable event
|
|
1999
|
+
if (this._webRTCSession.direction === 'incoming') {
|
|
2000
|
+
// return;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
this._callFinished();
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* @typedef {string} CALL_DIRECTION
|
|
2009
|
+
* @property {string} INBOUND Incoming call
|
|
2010
|
+
* @property {string} OUTBOUND Outgoing call
|
|
2011
|
+
* @property {string} TRANSFER Transferred call
|
|
2012
|
+
* @property {string} LOCAL Local call
|
|
2013
|
+
* @property {string} AUTOMATED Call triggered from api / server
|
|
2014
|
+
* @property {string} LISTEN Listen call
|
|
2015
|
+
*/
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* @typedef {string} CALL_DISPOSITION
|
|
2019
|
+
* @property {string} BUSY Number was busy
|
|
2020
|
+
* @property {string} INVALID_NUMBER Number call does not exist
|
|
2021
|
+
* @property {string} NO_ANSWER Call was not picked up before cancelling
|
|
2022
|
+
* @property {string} TEMP_UNAVAIL Remote is temporarily unavailable
|
|
2023
|
+
* @property {string} NORMAL Call was terminated normally
|
|
2024
|
+
* @property {string} FAILED There was a failure connecting the call
|
|
2025
|
+
* @property {string} BARRED Call was prohibited by server
|
|
2026
|
+
* @property {string} REJECTED Incoming call was rejected
|
|
2027
|
+
*/
|
|
2028
|
+
_callFinished() {
|
|
2029
|
+
this.log.info('Call finished');
|
|
2030
|
+
|
|
2031
|
+
// Fire callEnded event
|
|
2032
|
+
this._fireCallEndedEvent();
|
|
2033
|
+
|
|
2034
|
+
// Stop duration update timer
|
|
2035
|
+
clearInterval(this._durationUpdTimerObj);
|
|
2036
|
+
|
|
2037
|
+
// Stop rtcp stats uptime timer
|
|
2038
|
+
clearInterval(this._rtcpStatsTimer);
|
|
2039
|
+
|
|
2040
|
+
// Remove session from active sessions
|
|
2041
|
+
this._phone._sessionTerminated(this);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// Set answer call timeout
|
|
2045
|
+
_setAnswerCallTimeout(timeout) {
|
|
2046
|
+
this.log.debug('Setting answer call timeout to: ' + timeout);
|
|
2047
|
+
this._answerCallTimeout = timeout;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Fire recording state event
|
|
2051
|
+
_fireRecordingStateEvent() {
|
|
2052
|
+
/**
|
|
2053
|
+
* @event recordingStateChange
|
|
2054
|
+
* @type {object}
|
|
2055
|
+
* @property {boolean} state - New recording state
|
|
2056
|
+
*/
|
|
2057
|
+
try {
|
|
2058
|
+
this.emit('recordingStateChange', { state: this._isRecording, error: this._recordingError });
|
|
2059
|
+
} catch (e) {
|
|
2060
|
+
this.log.error('Exception in recording state change event: ' + e);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Fire mic mute state event
|
|
2065
|
+
_fireMuteStateEvent() {
|
|
2066
|
+
/**
|
|
2067
|
+
* @event muteStateChange
|
|
2068
|
+
* @type {object}
|
|
2069
|
+
* @property {boolean} state - Mute state (true = muted / false = unmuted)
|
|
2070
|
+
*/
|
|
2071
|
+
try {
|
|
2072
|
+
this.emit('muteStateChange', { state: this._isMuted });
|
|
2073
|
+
} catch (e) {
|
|
2074
|
+
this.log.error('Exception in mute state change event: ' + e);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// Fire recording state event
|
|
2079
|
+
_fireHoldStateEvent() {
|
|
2080
|
+
/**
|
|
2081
|
+
* @event holdStateChange
|
|
2082
|
+
* @type {object}
|
|
2083
|
+
* @property {boolean} state - New hold state
|
|
2084
|
+
*/
|
|
2085
|
+
try {
|
|
2086
|
+
this.emit('holdStateChange', { state: this._isOnHold });
|
|
2087
|
+
} catch (e) {
|
|
2088
|
+
this.log.error('Exception in hold state change event: ' + e);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Fire whisper mode event
|
|
2093
|
+
_fireWhisperModeChangeEvent() {
|
|
2094
|
+
/**
|
|
2095
|
+
* @event whisperModeChange
|
|
2096
|
+
* @type {object}
|
|
2097
|
+
* @property {boolean} mode - New whisper mode (0 = No whisper / 1 = Local only / 2 = Three way)
|
|
2098
|
+
*/
|
|
2099
|
+
try {
|
|
2100
|
+
this.emit('whisperModeChange', { mode: this._whisperMode });
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
this.log.error('Exception in whisper mode change event: ' + e);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Fire call data event to subscribers
|
|
2107
|
+
_fireCallEndedEvent() {
|
|
2108
|
+
/**
|
|
2109
|
+
* @event callEnded
|
|
2110
|
+
*
|
|
2111
|
+
* @type {object}
|
|
2112
|
+
* @property {uuid} uuid - Customer Call UUID for session
|
|
2113
|
+
* @param {CALL_DIRECTION} direction - Call direction
|
|
2114
|
+
* @param {CALL_DISPOSITION} disposition - The final call disposition
|
|
2115
|
+
* @param {string} originatingNumber - Caller ID number (a-number) used for call
|
|
2116
|
+
* @param {string} destinationNumber - Destination number (b-number) used for call
|
|
2117
|
+
* @param {Date} startTime - Start time of call
|
|
2118
|
+
* @param {Date} answerTime - Time call was answered
|
|
2119
|
+
* @param {Date} endTime - End time of call
|
|
2120
|
+
* @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
|
|
2121
|
+
* @param {String} durationStr - Total duration of the call formatted as (mm:ss)
|
|
2122
|
+
* @param {Number} answerDuration - Time before call was answered (millisec)
|
|
2123
|
+
* @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
|
|
2124
|
+
* @param {Number} activeDuration - Active duration of the call (time spend talking) (millisec)
|
|
2125
|
+
* @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
|
|
2126
|
+
* @param {object} callQualityReport - Call quality report (see callQualityReportUpdate event for details)
|
|
2127
|
+
*/
|
|
2128
|
+
|
|
2129
|
+
let e;
|
|
2130
|
+
|
|
2131
|
+
e = {
|
|
2132
|
+
uuid: this._callUUID,
|
|
2133
|
+
direction: this.direction,
|
|
2134
|
+
disposition: this._callDisposition,
|
|
2135
|
+
originatingNumber: this._originatingNumber,
|
|
2136
|
+
destinationNumber: this._destinationNumber,
|
|
2137
|
+
|
|
2138
|
+
// Calc durations
|
|
2139
|
+
duration: this._callEndTime - this._callBeginTime,
|
|
2140
|
+
answerDuration: this._callAnswerTime ? this._callAnswerTime - this._callBeginTime : null,
|
|
2141
|
+
activeDuration: this._callAnswerTime ? this._callEndTime - this._callAnswerTime : null,
|
|
2142
|
+
|
|
2143
|
+
// Convert timestamp into ISO8601
|
|
2144
|
+
startTime: new Date(this._callBeginTime).toISOString(),
|
|
2145
|
+
endTime: new Date(this._callEndTime).toISOString(),
|
|
2146
|
+
answerTime: this._callAnswerTime ? new Date(this._callAnswerTime).toISOString() : null,
|
|
2147
|
+
};
|
|
2148
|
+
|
|
2149
|
+
// Add some extra convenience fields
|
|
2150
|
+
e.durationStr = Tools.createDurationString(e.duration);
|
|
2151
|
+
e.answerDurationStr = e.answerDuration ? Tools.createDurationString(e.answerDuration) : '';
|
|
2152
|
+
e.activeDurationStr = e.activeDuration ? Tools.createDurationString(e.activeDuration) : '';
|
|
2153
|
+
e.callQualityReport = this._currentReport;
|
|
2154
|
+
|
|
2155
|
+
try {
|
|
2156
|
+
this.emit('callEnded', e);
|
|
2157
|
+
} catch (e) {
|
|
2158
|
+
this.log.error('Exception in callEnded event: ' + e);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Setup handling of remote audio
|
|
2163
|
+
_handleRemoteAudio(session) {
|
|
2164
|
+
// Stop any repeating tones if audio comes
|
|
2165
|
+
this._phone._endRepeatTone();
|
|
2166
|
+
|
|
2167
|
+
const self = this;
|
|
2168
|
+
session.connection.ontrack = function (event) {
|
|
2169
|
+
self.log.debug('Start media output (track):');
|
|
2170
|
+
self.log.debug(event);
|
|
2171
|
+
self._phone._remoteAudioElement.srcObject = event.streams[0];
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
</code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><div class="mobile-nav-links"></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="EventClient.html">EventClient</a></div><div class="sidebar-section-children"><a href="EventServiceNotConnected.html">EventServiceNotConnected</a></div><div class="sidebar-section-children"><a href="InvalidMediaDevice.html">InvalidMediaDevice</a></div><div class="sidebar-section-children"><a href="InvalidOptions.html">InvalidOptions</a></div><div class="sidebar-section-children"><a href="InvalidParameter.html">InvalidParameter</a></div><div class="sidebar-section-children"><a href="LogHandler.html">LogHandler</a></div><div class="sidebar-section-children"><a href="MediaNotPrepared.html">MediaNotPrepared</a></div><div class="sidebar-section-children"><a href="NoActiveSession.html">NoActiveSession</a></div><div class="sidebar-section-children"><a href="NotAllowed.html">NotAllowed</a></div><div class="sidebar-section-children"><a href="NotReady.html">NotReady</a></div><div class="sidebar-section-children"><a href="Phone.html">Phone</a></div><div class="sidebar-section-children"><a href="Request.html">Request</a></div><div class="sidebar-section-children"><a href="Requestor.html">Requestor</a></div><div class="sidebar-section-children"><a href="Session.html">Session</a></div><div class="sidebar-section-children"><a href="UnableToConnect.html">UnableToConnect</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-events"><div>Events</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#event:callDataInfo">callDataInfo</a></div><div class="sidebar-section-children"><a href="global.html#event:callDurationUpdate">callDurationUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:callEnded">callEnded</a></div><div class="sidebar-section-children"><a href="global.html#event:callQualityReportUpdate">callQualityReportUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:connectionStateChange">connectionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:holdStateChange">holdStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:incomingCall">incomingCall</a></div><div class="sidebar-section-children"><a href="global.html#event:muteStateChange">muteStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:recordingStateChange">recordingStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionCreated">sessionCreated</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionRemoved">sessionRemoved</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionStateChange">sessionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:whisperModeChange">whisperModeChange</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CALL_DIRECTION">CALL_DIRECTION</a></div><div class="sidebar-section-children"><a href="global.html#CALL_DISPOSITION">CALL_DISPOSITION</a></div><div class="sidebar-section-children"><a href="global.html#CONNECTION_STATE">CONNECTION_STATE</a></div><div class="sidebar-section-children"><a href="global.html#LOGLVL">LOGLVL</a></div><div class="sidebar-section-children"><a href="global.html#SESSION_STATE">SESSION_STATE</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>
|