@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.
Files changed (105) hide show
  1. package/README.md +102 -81
  2. package/demos/add_phone_number.html +8 -7
  3. package/demos/calldata_available.html +5 -5
  4. package/demos/embedded_config.html +3 -3
  5. package/demos/event_client.html +226 -211
  6. package/demos/phone_client.html +540 -514
  7. package/demos/styles.css +0 -0
  8. package/dist/telegenta.js +3663 -0
  9. package/dist/telegenta.min.js +1 -0
  10. package/docs/EventClient.html +3 -1093
  11. package/docs/EventServiceNotConnected.html +3 -0
  12. package/docs/InvalidMediaDevice.html +3 -0
  13. package/docs/InvalidOptions.html +3 -0
  14. package/docs/InvalidParameter.html +3 -0
  15. package/docs/LogHandler.html +7 -0
  16. package/docs/MediaNotPrepared.html +3 -0
  17. package/docs/NoActiveSession.html +3 -0
  18. package/docs/NotAllowed.html +3 -0
  19. package/docs/NotReady.html +3 -0
  20. package/docs/Phone.html +4 -2837
  21. package/docs/Request.html +3 -0
  22. package/docs/Requestor.html +3 -0
  23. package/docs/Session.html +3 -2599
  24. package/docs/UnableToConnect.html +3 -0
  25. package/docs/data/search.json +1 -0
  26. package/docs/event-client.js.html +352 -0
  27. package/docs/exceptions.js.html +104 -0
  28. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  29. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  30. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  31. package/docs/global.html +5 -4950
  32. package/docs/index.html +3 -66
  33. package/docs/logger.js.html +106 -0
  34. package/docs/phone-client.js.html +2175 -0
  35. package/docs/requestor.js.html +223 -0
  36. package/docs/scripts/core.js +726 -0
  37. package/docs/scripts/core.min.js +23 -0
  38. package/docs/scripts/resize.js +90 -0
  39. package/docs/scripts/search.js +265 -0
  40. package/docs/scripts/search.min.js +6 -0
  41. package/docs/scripts/{prettify → third-party}/Apache-License-2.0.txt +202 -202
  42. package/docs/scripts/third-party/fuse.js +9 -0
  43. package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  45. package/docs/scripts/third-party/hljs-original.js +5171 -0
  46. package/docs/scripts/third-party/hljs.js +1 -0
  47. package/docs/scripts/third-party/popper.js +5 -0
  48. package/docs/scripts/third-party/tippy.js +1 -0
  49. package/docs/scripts/third-party/tocbot.js +672 -0
  50. package/docs/scripts/third-party/tocbot.min.js +1 -0
  51. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  52. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  53. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  54. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  55. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  56. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  57. package/package.json +38 -26
  58. package/babel.config.js +0 -16
  59. package/browser/telegenta.min.js +0 -1
  60. package/demos/telegenta.min.js +0 -1
  61. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  62. package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
  63. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  64. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  65. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  66. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  67. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  68. package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
  69. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  70. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  71. package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
  72. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  73. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  74. package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  75. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  76. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  77. package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
  78. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  79. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  80. package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  81. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  82. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  83. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  84. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  85. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  86. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  87. package/docs/scripts/linenumber.js +0 -25
  88. package/docs/scripts/prettify/lang-css.js +0 -2
  89. package/docs/scripts/prettify/prettify.js +0 -28
  90. package/docs/styles/jsdoc-default.css +0 -692
  91. package/docs/styles/prettify-jsdoc.css +0 -111
  92. package/docs/styles/prettify-tomorrow.css +0 -132
  93. package/embedded_client/embedded_phone.js +0 -325
  94. package/embedded_client/index.html +0 -201
  95. package/embedded_client/server.php +0 -28
  96. package/embedded_client/styles.css +0 -310
  97. package/embedded_client/tiny_rest_client.php +0 -73
  98. package/embedded_client/webphone_sprites.png +0 -0
  99. package/es5/event-client.js +0 -277
  100. package/es5/index.js +0 -44
  101. package/es5/phone-client.js +0 -1336
  102. package/src/event-client.js +0 -1
  103. package/src/index.js +0 -10
  104. package/src/phone-client.js +0 -1
  105. 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.&lt;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 &amp;&amp; !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' &amp;&amp; c.relatedAddress !== null &amp;&amp; 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 &lt; 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 &lt; 50:
1888
+ report.qualityString = 'Bad';
1889
+ break;
1890
+ case report.rFactor &lt; 70:
1891
+ report.qualityString = 'Poor';
1892
+ break;
1893
+ case report.rFactor &lt; 80:
1894
+ report.qualityString = 'Fair';
1895
+ break;
1896
+ case report.rFactor &lt; 90:
1897
+ report.qualityString = 'Good';
1898
+ break;
1899
+ case report.rFactor &lt;= 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>