@telegenta/webclient 2.0.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +102 -81
  2. package/demos/add_phone_number.html +8 -7
  3. package/demos/calldata_available.html +5 -5
  4. package/demos/embedded_config.html +95 -95
  5. package/demos/event_client.html +226 -226
  6. package/demos/phone_client.html +564 -558
  7. package/dist/telegenta.js +3627 -0
  8. package/dist/telegenta.min.js +1 -0
  9. package/docs/EventClient.html +3 -1093
  10. package/docs/InvalidMediaDevice.html +3 -0
  11. package/docs/InvalidOptions.html +3 -0
  12. package/docs/InvalidParameter.html +3 -0
  13. package/docs/LogHandler.html +7 -0
  14. package/docs/MediaNotPrepared.html +3 -0
  15. package/docs/NoActiveSession.html +3 -0
  16. package/docs/NotAllowed.html +3 -0
  17. package/docs/NotReady.html +3 -0
  18. package/docs/Phone.html +4 -2837
  19. package/docs/Request.html +3 -0
  20. package/docs/Requestor.html +3 -0
  21. package/docs/Session.html +3 -2599
  22. package/docs/UnableToConnect.html +3 -0
  23. package/docs/data/search.json +1 -0
  24. package/docs/event-client.js.html +346 -0
  25. package/docs/exceptions.js.html +94 -0
  26. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  27. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  28. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  29. package/docs/global.html +5 -4950
  30. package/docs/index.html +3 -66
  31. package/docs/logger.js.html +106 -0
  32. package/docs/phone-client.js.html +2155 -0
  33. package/docs/requestor.js.html +223 -0
  34. package/docs/scripts/core.js +726 -0
  35. package/docs/scripts/core.min.js +23 -0
  36. package/docs/scripts/resize.js +90 -0
  37. package/docs/scripts/search.js +265 -0
  38. package/docs/scripts/search.min.js +6 -0
  39. package/docs/scripts/{prettify → third-party}/Apache-License-2.0.txt +202 -202
  40. package/docs/scripts/third-party/fuse.js +9 -0
  41. package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
  42. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  43. package/docs/scripts/third-party/hljs-original.js +5171 -0
  44. package/docs/scripts/third-party/hljs.js +1 -0
  45. package/docs/scripts/third-party/popper.js +5 -0
  46. package/docs/scripts/third-party/tippy.js +1 -0
  47. package/docs/scripts/third-party/tocbot.js +672 -0
  48. package/docs/scripts/third-party/tocbot.min.js +1 -0
  49. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  50. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  51. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  52. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  53. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  54. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  55. package/package.json +46 -34
  56. package/babel.config.js +0 -16
  57. package/browser/telegenta.min.js +0 -1
  58. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  59. package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
  60. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  61. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  62. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  63. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  64. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  65. package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
  66. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  67. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  68. package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
  69. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  70. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  71. package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  72. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  73. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  74. package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
  75. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  76. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  77. package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  78. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  79. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  80. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  81. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  82. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  83. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  84. package/docs/scripts/linenumber.js +0 -25
  85. package/docs/scripts/prettify/lang-css.js +0 -2
  86. package/docs/scripts/prettify/prettify.js +0 -28
  87. package/docs/styles/jsdoc-default.css +0 -692
  88. package/docs/styles/prettify-jsdoc.css +0 -111
  89. package/docs/styles/prettify-tomorrow.css +0 -132
  90. package/embedded_client/embedded_phone.js +0 -325
  91. package/embedded_client/index.html +0 -201
  92. package/embedded_client/server.php +0 -28
  93. package/embedded_client/styles.css +0 -310
  94. package/embedded_client/tiny_rest_client.php +0 -73
  95. package/embedded_client/webphone_sprites.png +0 -0
  96. package/es5/event-client.js +0 -322
  97. package/es5/index.js +0 -44
  98. package/es5/phone-client.js +0 -1739
  99. package/src/event-client.js +0 -1
  100. package/src/index.js +0 -10
  101. package/src/phone-client.js +0 -1
  102. package/webpack.config.js +0 -39
@@ -0,0 +1,2155 @@
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="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(`Autorejecting incoming call ${session.uuid} because there is already active calls`);
330
+ session.reject(486, "Busy");
331
+ return;
332
+ } else {
333
+ self.log.debug("There are no other sessions, adding incoming call");
334
+ }
335
+ }
336
+
337
+ self._addSession(session);
338
+
339
+ // Set new session state
340
+ session._setSessionState("INCOMING");
341
+
342
+ // Now fire incomingCall event
343
+ self._fireIncomingCallEvent(session);
344
+ }
345
+ });
346
+
347
+ // Setup connection state event subscriptions
348
+ this._UA.on("connecting", function (/* data */) {
349
+ self._setConnectionState("CONNECTING");
350
+ });
351
+
352
+ this._UA.on("connected", function (/* data */) {
353
+ self._setConnectionState("CONNECTED");
354
+ });
355
+
356
+ this._UA.on("disconnected", function (/* data */) {
357
+ self._setConnectionState("DISCONNECTED");
358
+ });
359
+
360
+ this._UA.on("registered", function (/* data */) {
361
+ self._setConnectionState("REGISTERED");
362
+ });
363
+
364
+ this._UA.on("unregistered", function (/* data */) {
365
+ self._setConnectionState("UNREGISTERED");
366
+ });
367
+
368
+ this._UA.on("registrationFailed", function (/* data */) {
369
+ self._setConnectionState("REGISTRATION_FAILED");
370
+ });
371
+
372
+ this._UA.start();
373
+ this._isStarted = true;
374
+ this.log.info("Successfully started phone");
375
+ }
376
+
377
+ /**
378
+ * Stops the phone client, gracefully disconnects from the server and terminates any active calls
379
+ * @throws NotReady - If phone is not started
380
+ */
381
+ stop() {
382
+ // Check phone is started first
383
+ if (!this._isStarted) {
384
+ throw new Exceptions.NotReady("Phone is not started");
385
+ }
386
+
387
+ // Make sure events are not firing anymore, we can't rely on GC.
388
+ this._UA.removeAllListeners();
389
+
390
+ // Stop the UA
391
+ this._UA.stop();
392
+ delete this._UA;
393
+
394
+ // Disconnect event client if needed
395
+ if (!this._eventClient) {
396
+ this.log.debug("Event client not running, not stopping");
397
+ return;
398
+ }
399
+ try {
400
+ this._eventClient.disconnect();
401
+ delete this._eventClient;
402
+ } catch (e) {
403
+ this.log.warning("Unable to disconnect event client");
404
+ }
405
+
406
+ this._isStarted = false;
407
+ this.log.info("Phone client successfully stopped");
408
+ }
409
+
410
+ /**
411
+ * Start a new call
412
+ * If you have other active calls they will be set on hold
413
+ * @param number Number to call
414
+ * @param params {object} call options
415
+ * @param {number} [params.record] Recording retention days. The number of days recording will be kept. Setting this to 0 disables recording.
416
+ * @param {number} [params.explicitCallerId] Force a specific caller id, by providing explicit phone number id
417
+ * @param {number} [params.explicitShortCallerId] Force a specific caller id, by providing explicit short caller id
418
+ * @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.
419
+ * @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.
420
+ * @param {string} [params.metaInfo] Add meta info to call, which will be available in server events and inside call complete webhook (max 256 chars)
421
+ * @return {Session} object for call
422
+ * @throws NotReady - If phone is not ready to call
423
+ * @throws NotAllowed - If there are calls currently connecting
424
+ *
425
+ */
426
+ call(number, params = {}) {
427
+ // Check phone is started first
428
+ if (!this._isStarted) {
429
+ throw new Exceptions.NotReady("Phone is not started");
430
+ }
431
+
432
+ // Check we dont have any other connecting calls
433
+ if (this._checkForConnectingCalls()) {
434
+ throw Exceptions.NotAllowed("Unable to make new calls while others are connecting");
435
+ }
436
+
437
+ // Hold other calls if needed
438
+ this.holdAllCalls();
439
+
440
+ const callParams = Object.assign(
441
+ {
442
+ record: false,
443
+ maximumCallCost: 0,
444
+ answerCallTimeout: 0,
445
+ explicitCallerId: null,
446
+ explicitShortCallerId: null,
447
+ metaInfo: null,
448
+ _isListenCall: false,
449
+ },
450
+ params,
451
+ );
452
+
453
+ // Trim phone number to remove spaces and '()'
454
+ number = number.trim().replace(/ |\(|\)/g, "");
455
+
456
+ this.log.info("Calling number: " + number);
457
+
458
+ let session = new Session(this);
459
+ session.newOutboundSession(number, callParams);
460
+ this._addSession(session);
461
+ return session;
462
+ }
463
+
464
+ /**
465
+ * Start a new session and listen to another call
466
+ * @param listenKey provided by API endpoint "active-calls" and the event library subscription for resource name "activecall"
467
+ * @return {Session} object for listen
468
+ * @throws NotReady - If phone is not ready to call
469
+ * @throws NotAllowed - If there are calls currently connecting
470
+ */
471
+ listen(listenKey) {
472
+ const s = listenKey.split("@");
473
+ let callUUID = s[0];
474
+ let mediaSwitch = "sip:" + s[1];
475
+
476
+ this.log.debug("Initiating listen to call with uuid " + callUUID + " on media switch " + mediaSwitch);
477
+ return this.call("listen-" + callUUID, {
478
+ mediaSwitchURI: mediaSwitch,
479
+ _isListenCall: true,
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Get session by UUID, if none are found null is returned
485
+ * @param uuid
486
+ * @returns {Session|null}
487
+ */
488
+ getSessionByUUID(uuid) {
489
+ for (let key in this._activeSessions) {
490
+ if (this._activeSessions.hasOwnProperty(key)) {
491
+ if (this._activeSessions[key].uuid === uuid) {
492
+ return this._activeSessions[key];
493
+ }
494
+ }
495
+ }
496
+
497
+ return null;
498
+ }
499
+
500
+ /**
501
+ * Return phone options
502
+ * @returns {phoneOptions}
503
+ */
504
+ get options() {
505
+ return this._options;
506
+ }
507
+
508
+ /**
509
+ * Return list of current sessions
510
+ * @returns [Session]
511
+ */
512
+ get sessions() {
513
+ let s = [];
514
+ for (let key in this._activeSessions) {
515
+ if (this._activeSessions.hasOwnProperty(key)) {
516
+ s.push(this._activeSessions[key]);
517
+ }
518
+ }
519
+
520
+ return s;
521
+ }
522
+
523
+ /**
524
+ * Return media permission state
525
+ * @return {boolean}
526
+ */
527
+ get hasMediaPermission() {
528
+ return this._mediaPermission;
529
+ }
530
+
531
+ /**
532
+ * Media permission denied
533
+ * @return {boolean}
534
+ */
535
+ get hasMediaPermissionError() {
536
+ return this._mediaPermissionError;
537
+ }
538
+
539
+ /**
540
+ * Phone client started
541
+ * @return {boolean}
542
+ */
543
+ get isStarted() {
544
+ return this._isStarted;
545
+ }
546
+
547
+ /**
548
+ * Verify browser is supported
549
+ * @return true if browser supported
550
+ */
551
+ get browserSupported() {
552
+ return Tools.browserIsChrome() || Tools.browserIsFirefox() || Tools.browserIsOpera() || Tools.browserIsSafari();
553
+ }
554
+
555
+ /**
556
+ * Remote audio HTML element
557
+ */
558
+ get remoteAudioElement() {
559
+ return this._remoteAudioElement;
560
+ }
561
+
562
+ // TONE STUFF
563
+ /**
564
+ * Play tone locally (on incoming remote audio)
565
+ * @param duration Duration of tone in ms
566
+ * @param freq1 Frequency in Hz of tone 1
567
+ * @param freq2 Frequency in Hz of tone 2
568
+ * @param callback Function to be called after tone has ended
569
+ * @throws NotAllowed - If audio api is not supported by browser
570
+ */
571
+ playTone(duration, freq1, freq2 = 0, callback = null) {
572
+ this.log.debug(`Playing tone ${freq1} / ${freq2} for ${duration} ms`);
573
+
574
+ this._oscillator1.frequency.value = freq1;
575
+ this._oscillator2.frequency.value = freq2;
576
+ this._toneGainProcessor.gain.value = this._options.toneVolume;
577
+
578
+ // Set timer to stop tone again
579
+ const self = this;
580
+ this._toneTimer = setTimeout(function () {
581
+ self.stopTone();
582
+
583
+ // Handle callback now
584
+ if (callback) {
585
+ callback();
586
+ }
587
+ }, duration);
588
+ }
589
+
590
+ /**
591
+ * Immediately stop playing tones
592
+ * (Tone callbacks will be cancelled)
593
+ */
594
+ stopTone() {
595
+ this._oscillator1.frequency.value = 0;
596
+ this._oscillator2.frequency.value = 0;
597
+ this._toneGainProcessor.gain.value = 0;
598
+ clearTimeout(this._toneTimer);
599
+ }
600
+
601
+ // Begin repeating tones
602
+ _startRepeatTone(sequences) {
603
+ this.log.info("Repeating tone sequence started");
604
+ this.log.debug(sequences);
605
+ this._repeatToneSequence = sequences;
606
+ this._repeatToneSequencePos = 0;
607
+ this._repeatToneActive = true;
608
+ this._doRepeatTone();
609
+ }
610
+
611
+ // Do one tone
612
+ _doRepeatTone() {
613
+ if (this._repeatToneActive) {
614
+ this.log.debug(`Playing repeated tone, pos = ${this._repeatToneSequencePos}`);
615
+
616
+ // Get tone definitions and move to next sequence position
617
+ let toneDef = this._repeatToneSequence[this._repeatToneSequencePos];
618
+ this._repeatToneSequencePos++;
619
+ if (this._repeatToneSequencePos === this._repeatToneSequence.length) {
620
+ this._repeatToneSequencePos = 0;
621
+ }
622
+
623
+ const self = this;
624
+ this.playTone(toneDef.d, toneDef.f1, toneDef.f2, function () {
625
+ setTimeout(self._doRepeatTone.bind(self), toneDef.g);
626
+ });
627
+ }
628
+ }
629
+
630
+ // End repeating tones
631
+ _endRepeatTone() {
632
+ if (this._repeatToneActive) {
633
+ this.log.info("Repeating tone stopped");
634
+ this._repeatToneActive = false;
635
+ this.stopTone();
636
+ }
637
+ }
638
+
639
+ // Initialize tone generator
640
+ _initToneGenerator() {
641
+ // Create audio context
642
+ const AudioContext = window.AudioContext || window.webkitAudioContext || false;
643
+ if (!AudioContext) {
644
+ throw new Exceptions.NotAllowed("Audio API not supported by this browser");
645
+ }
646
+
647
+ const ctx = new AudioContext();
648
+ this._toneStreamNode = ctx.createMediaStreamDestination();
649
+ this._toneAudioElement.srcObject = this._toneStreamNode.stream;
650
+
651
+ this._toneGainProcessor = ctx.createGain();
652
+ this._toneGainProcessor.gain.value = 0;
653
+ this._toneGainProcessor.connect(this._toneStreamNode);
654
+
655
+ // Create two oscillators and start them
656
+ this._oscillator1 = ctx.createOscillator();
657
+ this._oscillator1.connect(this._toneGainProcessor);
658
+ this._oscillator1.frequency.value = 0;
659
+ this._oscillator1.start();
660
+
661
+ this._oscillator2 = ctx.createOscillator();
662
+ this._oscillator2.connect(this._toneGainProcessor);
663
+ this._oscillator2.frequency.value = 0;
664
+ this._oscillator2.start();
665
+ }
666
+
667
+ /**
668
+ * Hold all active calls
669
+ * @throws NotAllowed If there are connection calls
670
+ */
671
+ holdAllCalls() {
672
+ this.log.info("Holding all active sessions");
673
+ // Check all session can be set on hold
674
+ if (this._checkForConnectingCalls()) {
675
+ throw Exceptions.NotAllowed("A session is currently connecting");
676
+ }
677
+
678
+ // Iterate through all active calls and hold them
679
+ for (let key in this._activeSessions) {
680
+ if (this._activeSessions.hasOwnProperty(key)) {
681
+ let session = this._activeSessions[key];
682
+ if (session.state === "ACTIVE") {
683
+ session.hold();
684
+ }
685
+ }
686
+ }
687
+ }
688
+
689
+ /*
690
+ * ======= PRIVATE METHODS PHONE CLASS =========================================================================
691
+ */
692
+
693
+ // Add a new session
694
+ _addSession(session) {
695
+ /**
696
+ * @event sessionCreated
697
+ * @type {object}
698
+ * @param {session} session - New session created
699
+ */
700
+ // Give session unique id
701
+ this._sessionCounter++;
702
+ session.__containerId = this._sessionCounter;
703
+
704
+ this.log.info(`Adding new session ${session.__containerId}: ${session.uuid}`);
705
+ this._activeSessions[session.__containerId] = session;
706
+ this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions:`);
707
+ this.log.debug(this._activeSessions);
708
+
709
+ // Fire event
710
+ try {
711
+ this.emit("sessionCreated", { session: session });
712
+ } catch (e) {
713
+ this.log.error("Exception in sessionCreated event: " + e);
714
+ }
715
+ }
716
+
717
+ // Remove session
718
+ _removeSession(session) {
719
+ /**
720
+ * @event sessionRemoved
721
+ * @type {object}
722
+ * @param {session} session - Session removed
723
+ */
724
+
725
+ this.log.info(`Removing session ${session.__containerId}: ${session.uuid}`);
726
+
727
+ // Fire event
728
+ try {
729
+ this.emit("sessionRemoved", { session: session });
730
+ } catch (e) {
731
+ this.log.error("Exception in sessionRemoved event: " + e);
732
+ }
733
+
734
+ delete this._activeSessions[session.__containerId];
735
+ this.log.debug(`We now have ${Object.keys(this._activeSessions).length} sessions`);
736
+ this.log.debug(this._activeSessions);
737
+
738
+ // Stop any repeating tones
739
+ this._endRepeatTone();
740
+ }
741
+
742
+ // Session call this when call is finished
743
+ _sessionTerminated(session) {
744
+ this.log.debug(`Session ${session.uuid} terminated, removing in ${this._options.autoRemoveSessionTime} ms`);
745
+ const self = this;
746
+ setTimeout(function () {
747
+ self._removeSession(session);
748
+ }, this._options.autoRemoveSessionTime);
749
+ }
750
+
751
+ // Return true if there are calls currently connecting
752
+ _checkForConnectingCalls() {
753
+ for (let key in this._activeSessions) {
754
+ if (this._activeSessions.hasOwnProperty(key)) {
755
+ let session = this._activeSessions[key];
756
+ if (session.state === "INITIALIZING" || session.state === "CALLING" || session.state === "PROGRESS" || session.state === "INCOMING") {
757
+ return true;
758
+ }
759
+ }
760
+ }
761
+ return false;
762
+ }
763
+
764
+ /**
765
+ * @typedef {string} CONNECTION_STATE
766
+ * @property {string} CONNECTING Connecting to server
767
+ * @property {string} CONNECTED Connected to server
768
+ * @property {string} DISCONNECTED Disconnected from server
769
+ * @property {string} REGISTERED Registered on server
770
+ * @property {string} UNREGISTERED De-registered from server
771
+ * @property {string} REGISTRATION_FAILED Registration failed
772
+ */
773
+ // Set connection state and emit event
774
+ _setConnectionState(state) {
775
+ /**
776
+ * @event connectionStateChange
777
+ * @type {object}
778
+ * @param {CONNECTION_STATE} state - The current connection state
779
+ */
780
+
781
+ this.log.info("Connection state: " + state);
782
+ this._connectionState = state;
783
+ try {
784
+ this.emit("connectionStateChange", { state: state });
785
+ } catch (e) {
786
+ this.log.error("Exception in connectionStateChange event: " + e);
787
+ }
788
+ }
789
+
790
+ /**
791
+ * @event incomingCall
792
+ * @type {object}
793
+ * @param {Session} session New incoming session
794
+ */
795
+ _fireIncomingCallEvent(session) {
796
+ try {
797
+ this.emit("incomingCall", session);
798
+ } catch (e) {
799
+ this.log.error("Exception in incomingCall event: " + e);
800
+ }
801
+ }
802
+
803
+ // Fire call data event to subscribers
804
+ _fireCallDataInfoEvent(eventData = null) {
805
+ /**
806
+ * @event callDataInfo
807
+ *
808
+ * @type {object}
809
+ * @property {uuid} uuid - Customer Call UUID for session
810
+ * @param {CALL_DIRECTION} direction - Call direction
811
+ * @param {CALL_DISPOSITION} disposition - The final call disposition
812
+ * @param {string} originatingNumber - Caller ID number (a-number) used for call
813
+ * @param {string} destinationNumber - Destination number (b-number) used for call
814
+ * @param {Date} startTime - Start time of call
815
+ * @param {Date} answerTime - Time call was answered
816
+ * @param {Date} endTime - End time of call
817
+ * @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
818
+ * @param {String} durationStr - Total duration of the call formatted as (mm:ss)
819
+ * @param {number} answerDuration - Time before call was answered (millisec)
820
+ * @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
821
+ * @param {number} activeDuration - Active duration of the call (time spend talking) (millisec)
822
+ * @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
823
+ * @param {number} rateDestinationId - Rate destination id
824
+ * @param {string} rateDestinationStr - Rate destination name
825
+ * @param {number} rateDestinationCost - Rate destination cost / minute
826
+ * @param {number} callConnectFee - Call attempt/connect cost
827
+ * @param {number} effectiveCidId - Phone number id used as a-number for call
828
+ * @param {number} effectiveCidNumberStr - Phone number used as a-number for call
829
+ * @param {string} effectiveCidNameStr - Phone number name used as a-number for call
830
+ * @param {boolean} callPrivacy - Call privacy used for call (number hidden)
831
+ * @param {number} hangupCauseQ850 - Q.850 hangup cause code
832
+ * @param {Object[]} recordings - List of recordings done for this call
833
+ * @param {number} recordings[].startTime - Time recording was started
834
+ * @param {number} recordings[].length - Recording length in milliseconds
835
+ * @param {number} recordings[].size - Recording size (bytes)
836
+ * @param {number} recordings[].urlPlayer - URL for recording with player
837
+ * @param {number} recordings[].url - URL for recording without player
838
+ * @param {number} recordings[].retentionDate - Date recording will be automatically deleted
839
+ */
840
+
841
+ let e;
842
+ e = camelcaseKeys(eventData);
843
+ e.recordings = camelcaseKeys(eventData.recordings);
844
+
845
+ // Event all duration fields are in seconds, convert to millisecs
846
+ e.duration = e.duration * 1000;
847
+ e.answerDuration = e.answerDuration * 1000;
848
+ e.activeDuration = e.activeDuration * 1000;
849
+
850
+ // Add some extra convenience fields
851
+ e.durationStr = Tools.createDurationString(e.duration);
852
+ e.answerDurationStr = e.answerDuration ? Tools.createDurationString(e.answerDuration) : "";
853
+ e.activeDurationStr = e.activeDuration ? Tools.createDurationString(e.activeDuration) : "";
854
+ try {
855
+ this.emit("callDataInfo", e);
856
+ } catch (e) {
857
+ this.log.error("Exception in callDataInfo event: " + e);
858
+ }
859
+ }
860
+
861
+ // Check if we have media permission
862
+ _checkMediaPermission() {
863
+ // Check media is prepared
864
+ if (!this._mediaPermission) {
865
+ throw new Exceptions.MediaNotPrepared();
866
+ }
867
+ }
868
+ }
869
+
870
+ /** Session class created for every active call. You should use either call() or event incomingCall() to get new sessions
871
+ *
872
+ * @fires callEnded Receive basic information about call after call has been terminated
873
+ * @fires callDurationUpdate Regular updates on call duration (every second)
874
+ * @fires sessionStateChange Changes to session state
875
+ * @fires recordingStateChange When recording state changes
876
+ * @fires muteStateChange When mute state changes
877
+ * @fires holdStateChange When call is hold/unload
878
+ *
879
+ */
880
+ export class Session extends EventEmitter {
881
+ constructor(phone) {
882
+ super();
883
+
884
+ // Parent Phone object
885
+ this._phone = phone;
886
+
887
+ // Use log instance from phone object
888
+ this.log = phone.log;
889
+
890
+ this._callUUID = null;
891
+ this._webRTCSession = null;
892
+ this._sessionState = null;
893
+ this._iceCandidateTimer = null;
894
+ this._isOnHold = false;
895
+
896
+ // Audio device flags
897
+ this._isMuted = false;
898
+ this._isRecording = false;
899
+ this._recordingError = null;
900
+
901
+ // Call timers
902
+ this._callBeginTime = null;
903
+ this._callAnswerTime = null;
904
+ this._callEndTime = null;
905
+ this._durationUpdTimerObj = null;
906
+
907
+ // Call information
908
+ this._originatingNumber = null;
909
+ this._destinationNumber = null;
910
+ this._callDisposition = null;
911
+ this._localNumberName = null;
912
+ this._isListenCall = false;
913
+ this._whisperMode = null;
914
+ this._manualDisposition = null;
915
+
916
+ // Quality report
917
+ this._rtcpStatsTimer = null;
918
+ this._currentReport = null;
919
+
920
+ // Timeout
921
+ this._answerCallTimeout = 0;
922
+ this._answerCallTimer = null;
923
+
924
+ // Current media switch handling the call
925
+ this._mediaSwitch = null;
926
+ }
927
+
928
+ // Create a new outbound session
929
+ newOutboundSession(number, callParams) {
930
+ // First create a UUID
931
+ this._callUUID = Tools.createUUID();
932
+ this.log.debug(`Created new outbound session to: ${number} [${this._callUUID}]`);
933
+ this.destinationNumber = number;
934
+
935
+ let options = {
936
+ mediaConstraints: { audio: true, video: false },
937
+ pcConfig: {
938
+ iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
939
+ },
940
+ extraHeaders: ["X-Call-token: " + this._phone.options.callToken, "X-P-UUID: " + this._callUUID, "X-Max-cost: " + callParams.maximumCallCost],
941
+ };
942
+
943
+ // Append explicit caller id if needed
944
+ if (callParams.explicitCallerId) {
945
+ options.extraHeaders.push("X-CID-Number-Id: " + callParams.explicitCallerId);
946
+ }
947
+
948
+ // Append explicit short cid if needed
949
+ if (callParams.explicitShortCallerId) {
950
+ options.extraHeaders.push("X-Short-CID-Id: " + callParams.explicitShortCallerId);
951
+ }
952
+
953
+ // Append meta info
954
+ if (callParams.metaInfo) {
955
+ options.extraHeaders.push("X-Meta-Info: " + encodeURI(callParams.metaInfo.toString().substr(0, 255)));
956
+ }
957
+
958
+ // Enable recording?
959
+ if (callParams.record) {
960
+ if (isNaN(callParams.record)) {
961
+ throw new Exceptions.InvalidParameter("Record parameter must be a number");
962
+ }
963
+ this.log.debug(`Recording enabled and kept for ${callParams.record} days`);
964
+ options.extraHeaders.push("X-Record: " + callParams.record);
965
+ this._isRecording = true;
966
+ } else {
967
+ this._isRecording = false;
968
+ }
969
+
970
+ // Store listen flag
971
+ this._isListenCall = callParams._isListenCall;
972
+
973
+ // Manually specified media switch
974
+ if (callParams.mediaSwitchURI) {
975
+ this.log.debug("Requesting specific media switch " + callParams.mediaSwitchURI);
976
+ options.extraHeaders.push("X-RMS: " + callParams.mediaSwitchURI);
977
+ } else {
978
+ // Request specific media switch to group calls on same server
979
+ const otherSessions = this.getOtherSessions();
980
+
981
+ if (otherSessions.length > 0) {
982
+ if (otherSessions[0]._mediaSwitch) {
983
+ // There are existing active calls, append first media switch address found and request call grouping to this
984
+ this.log.debug("Requesting call grouping on media switch " + otherSessions[0]._mediaSwitch);
985
+ options.extraHeaders.push("X-RMS: sip:" + otherSessions[0]._mediaSwitch);
986
+ } else {
987
+ this.log.warning("Other sessions exist but media switch address is invalid");
988
+ }
989
+ } else {
990
+ this.log.debug("No other active calls, no media switch grouping requested");
991
+ }
992
+ }
993
+
994
+ // Prepare invite uri
995
+ const uri = "sip:" + number + "@telegenta.com";
996
+
997
+ // Not start call and create sip session
998
+ try {
999
+ this._initWebRTCSession(this._phone._UA.call(uri, options));
1000
+ } catch (error) {
1001
+ this.log.error("Error starting call:", error);
1002
+ return null;
1003
+ }
1004
+
1005
+ if (callParams.answerCallTimeout) {
1006
+ this._setAnswerCallTimeout(callParams.answerCallTimeout);
1007
+ }
1008
+ }
1009
+
1010
+ // Create new inbound session
1011
+ newInboundSession(newSessionRequest) {
1012
+ this._callUUID = newSessionRequest.request.getHeader("X-P-UUID", 0);
1013
+ this._destinationNumber = newSessionRequest.request.getHeader("X-DID", 0);
1014
+ this._localNumberName = unescape(newSessionRequest.request.getHeader("X-DID-NAME", 0));
1015
+ this._originName = newSessionRequest.request.from.display_name;
1016
+ this._originatingNumber = newSessionRequest.request.from.uri.user;
1017
+ this.log.info(`Incoming call from ${this._originatingNumber} [${this._callUUID}]`);
1018
+ this.log.debug(newSessionRequest);
1019
+
1020
+ // Get media switch info
1021
+ this._mediaSwitch = newSessionRequest.request.getHeader("X-MS");
1022
+ this.log.debug(`Media switch handling call: ${this._mediaSwitch}`);
1023
+
1024
+ this._initWebRTCSession(newSessionRequest.session);
1025
+ }
1026
+
1027
+ /**
1028
+ * Return a list of all sessions except this session
1029
+ * Mostly used when transferring call
1030
+ */
1031
+ getOtherSessions() {
1032
+ const allSessions = this._phone.sessions;
1033
+
1034
+ let s = [];
1035
+ for (let key in allSessions) {
1036
+ if (allSessions[key]) {
1037
+ if (allSessions[key].__containerId !== this.__containerId) {
1038
+ s.push(allSessions[key]);
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ return s;
1044
+ }
1045
+
1046
+ // Initialize with WebRTCSession object
1047
+ _initWebRTCSession(webRTCSession) {
1048
+ this._webRTCSession = webRTCSession;
1049
+ this._callBeginTime = Date.now();
1050
+
1051
+ // Hookup events
1052
+ const self = this;
1053
+
1054
+ // ICE CANDIDATES
1055
+ this._webRTCSession.on("icecandidate", function (event) {
1056
+ const c = event.candidate;
1057
+ self.log.debug(`Got ICE candidate: ${c.candidate}`);
1058
+ if (c.type === "srflx" &amp;&amp; c.relatedAddress !== null &amp;&amp; c.relatedPort !== null) {
1059
+ self.log.info(`Accepting ICE candidate: ${c.candidate}`);
1060
+
1061
+ // Clear timer first
1062
+ if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
1063
+
1064
+ // Now fire ready
1065
+ event.ready();
1066
+ } else {
1067
+ // Start timeout
1068
+ if (self._iceCandidateTimer != null) clearTimeout(self._iceCandidateTimer);
1069
+
1070
+ self._iceCandidateTimer = setTimeout(
1071
+ function (data) {
1072
+ self.log.debug(`ICE Timeout (${self._phone.options.iceGatherTimeout} ms) reached`);
1073
+ data.ready();
1074
+ },
1075
+ self._phone.options.iceGatherTimeout,
1076
+ event,
1077
+ );
1078
+ }
1079
+ });
1080
+
1081
+ this._webRTCSession.on("connecting", function (/* data */) {
1082
+ self._startDurationUpdateTimer();
1083
+ self._startRTCPStatsUpdateTimer();
1084
+ self._setSessionState("INITIALIZING");
1085
+ });
1086
+
1087
+ this._webRTCSession.on("sending", function (/* data */) {
1088
+ // Play call initialized tone if needed
1089
+ if (self._phone.options.playCallInitializedTone) {
1090
+ self._phone._startRepeatTone(ST_CALLINIT);
1091
+ }
1092
+
1093
+ self._setSessionState("CALLING");
1094
+ });
1095
+
1096
+ this._webRTCSession.on("progress", function (data) {
1097
+ if (data.originator === "remote") {
1098
+ self._setSessionState("PROGRESS", { code: data.response.status_code });
1099
+ self._phone._endRepeatTone();
1100
+
1101
+ // Ringback handling on 180 RINGING
1102
+ if (data.response.status_code === 180) {
1103
+ self._phone._startRepeatTone(ST_RINGBACK);
1104
+ }
1105
+
1106
+ // Also start answer call timeout
1107
+ if (self._answerCallTimeout) {
1108
+ self._answerCallTimer = setTimeout(function () {
1109
+ self.log.info(`Answer call timeout after ${self._answerCallTimeout} seconds`);
1110
+ try {
1111
+ self.hangup();
1112
+ } catch (e) {
1113
+ self.log.info("No active session, ignoring answer call timeout");
1114
+ }
1115
+ }, self._answerCallTimeout * 1000);
1116
+ }
1117
+ }
1118
+ });
1119
+
1120
+ this._webRTCSession.on("accepted", function (data) {
1121
+ // Always end ringsback
1122
+ self._phone._endRepeatTone();
1123
+
1124
+ self._callAnswerTime = Date.now();
1125
+
1126
+ // Call was answered, clear answer call timeout timer if needed
1127
+ if (self._answerCallTimeout) {
1128
+ clearTimeout(self._answerCallTimer);
1129
+ self._answerCallTimeout = 0;
1130
+ }
1131
+
1132
+ // Get initial recording state
1133
+ if (data.originator === "remote") {
1134
+ const recState = data.response.getHeader("X-Recording");
1135
+ if (recState === "true") {
1136
+ self.log.debug("Call is being prerecorded");
1137
+ // self._isRecording = true;
1138
+ // self.emit('recordingStateChange', {state: true, error: null});
1139
+ } else {
1140
+ self.log.debug("No call recording started yet");
1141
+ }
1142
+
1143
+ // Get media switch info
1144
+ self._mediaSwitch = data.response.getHeader("X-MS");
1145
+ self.log.debug(`Media switch handling call: ${self._mediaSwitch}`);
1146
+ }
1147
+ });
1148
+
1149
+ this._webRTCSession.on("confirmed", function (/* data */) {
1150
+ self._setSessionState("ACTIVE");
1151
+ });
1152
+
1153
+ this._webRTCSession.on("ended", function (/* data */) {
1154
+ // End any ringsbacks if needed
1155
+ self._phone._endRepeatTone();
1156
+
1157
+ self._callEndTime = Date.now();
1158
+
1159
+ // Clear answer call timeout timer if needed
1160
+ if (self._answerCallTimeout) {
1161
+ clearTimeout(self._answerCallTimer);
1162
+ }
1163
+
1164
+ self._setSessionState("TERMINATED");
1165
+ });
1166
+
1167
+ this._webRTCSession.on("failed", function (data) {
1168
+ // End any ringsbacks if needed
1169
+ self._phone._endRepeatTone();
1170
+
1171
+ // self.log.error(data);
1172
+ self._callEndTime = Date.now();
1173
+
1174
+ // Clear answer call timeout timer if needed
1175
+ if (self._answerCallTimeout) {
1176
+ clearTimeout(self._answerCallTimer);
1177
+ }
1178
+
1179
+ let m;
1180
+
1181
+ // Append message if available
1182
+ if (data.message) {
1183
+ m = `${data.message.status_code} ${data.message.reason_phrase}`;
1184
+ } else {
1185
+ m = "";
1186
+ }
1187
+
1188
+ self._setSessionState("FAILED", { cause: data.cause, message: m });
1189
+ });
1190
+
1191
+ this._webRTCSession.on("dtmf", function (data) {
1192
+ self.log.debug("Session DTMF", data);
1193
+ });
1194
+
1195
+ this._webRTCSession.on("newInfo", function (data) {
1196
+ self.log.debug("New session info", data);
1197
+ });
1198
+
1199
+ this._webRTCSession.on("hold", function (/* data */) {
1200
+ self._isOnHold = true;
1201
+ self._fireHoldStateEvent();
1202
+ self._setSessionState("ON_HOLD");
1203
+ });
1204
+
1205
+ this._webRTCSession.on("unhold", function (/* data */) {
1206
+ self._isOnHold = false;
1207
+ self._fireHoldStateEvent();
1208
+ self._setSessionState("ACTIVE");
1209
+ });
1210
+
1211
+ this._webRTCSession.on("muted", function (data) {
1212
+ self.log.debug("Microphone muted", data);
1213
+ self._isMuted = true;
1214
+ self._fireMuteStateEvent();
1215
+ });
1216
+
1217
+ this._webRTCSession.on("unmuted", function (data) {
1218
+ self.log.debug("Microphone unmuted", data);
1219
+ self._isMuted = false;
1220
+ self._fireMuteStateEvent();
1221
+ });
1222
+
1223
+ this._webRTCSession.on("reinvite", function (data) {
1224
+ self.log.debug("Session reinvite", data);
1225
+ });
1226
+
1227
+ this._webRTCSession.on("update", function (data) {
1228
+ self.log.debug("Session update", data);
1229
+ });
1230
+
1231
+ this._webRTCSession.on("getusermediafailed", function (data) {
1232
+ self.log.error("Getusermedia failed", data);
1233
+ });
1234
+
1235
+ // If connection connect audio stream
1236
+ if (self._webRTCSession.connection) {
1237
+ self._handleRemoteAudio(self._webRTCSession);
1238
+ }
1239
+
1240
+ // DEBUG STUFF
1241
+ this.log.debug("WebRTCSession object:");
1242
+ this.log.debug(this);
1243
+ }
1244
+
1245
+ /**
1246
+ * Hangup call
1247
+ * @param {CALL_DISPOSITION} disposition - Hangup call with specific disposition
1248
+ * @throws NoActiveSession - If no active session / call exists
1249
+ */
1250
+ hangup(disposition = null) {
1251
+ if (!disposition) {
1252
+ this.log.info("Hanging up call");
1253
+ this._webRTCSession.terminate();
1254
+ } else {
1255
+ this.log.info(`Hanging up call with disposition ${disposition}`);
1256
+ this._manualDisposition = disposition;
1257
+ this._webRTCSession.terminate({
1258
+ extraHeaders: ["X-HDISP: " + disposition],
1259
+ });
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ * Answer incoming call
1265
+ * @param metaInfo - Add meta info to this call
1266
+ */
1267
+ answer(metaInfo = null) {
1268
+ let options = {
1269
+ mediaConstraints: { audio: true, video: false },
1270
+ pcConfig: {
1271
+ iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
1272
+ },
1273
+ extraHeaders: ["X-Call-token: " + this._phone.options.callToken],
1274
+ };
1275
+
1276
+ // Append meta info if needed
1277
+ if (metaInfo) {
1278
+ options.extraHeaders.push("X-Meta-Info: " + encodeURI(metaInfo.toString().substr(0, 255)));
1279
+ }
1280
+
1281
+ this._webRTCSession.answer(options);
1282
+
1283
+ // After answer we start remote audio
1284
+ this._handleRemoteAudio(this._webRTCSession);
1285
+ }
1286
+
1287
+ /**
1288
+ * Reject incoming call
1289
+ * @param statusCode - SIP Status code
1290
+ * @param reasonPhrase - Reason phrase
1291
+ */
1292
+ reject(statusCode = 603, reasonPhrase = "Rejected call") {
1293
+ this.log.info("Rejecting call");
1294
+ this._webRTCSession.terminate({
1295
+ status_code: statusCode,
1296
+ reason_phrase: reasonPhrase,
1297
+ });
1298
+ }
1299
+
1300
+ /**
1301
+ * Return UUID of call
1302
+ * @returns {string} UUID of call
1303
+ */
1304
+ get uuid() {
1305
+ return this._callUUID;
1306
+ }
1307
+
1308
+ /**
1309
+ * Destination number called
1310
+ * @returns {string} called number
1311
+ */
1312
+ get destinationNumber() {
1313
+ return this._destinationNumber;
1314
+ }
1315
+
1316
+ /**
1317
+ * Name of local number (for inbound)
1318
+ * @returns {string} Name of local number called
1319
+ */
1320
+ get localNumberName() {
1321
+ return this._localNumberName;
1322
+ }
1323
+
1324
+ /**
1325
+ * Originating number
1326
+ * @returns {string} Number of calling party
1327
+ */
1328
+ get originatingNumber() {
1329
+ return this._originatingNumber;
1330
+ }
1331
+
1332
+ // Set destination number
1333
+ set destinationNumber(number) {
1334
+ this.log.debug("Setting destination number to: " + number);
1335
+ this._destinationNumber = number;
1336
+ }
1337
+
1338
+ /**
1339
+ * Session state
1340
+ * @returns {SESSION_STATE} current session state
1341
+ */
1342
+ get state() {
1343
+ return this._sessionState;
1344
+ }
1345
+
1346
+ /**
1347
+ * Session call direction
1348
+ * @returns {CALL_DIRECTION} current session state
1349
+ */
1350
+ get direction() {
1351
+ if (this._isListenCall) {
1352
+ this.log.debug("Call direction is LISTEN");
1353
+ return "LISTEN";
1354
+ } else if (this._webRTCSession.direction === "incoming") {
1355
+ this.log.debug("Call direction is INBOUND");
1356
+ return "INBOUND";
1357
+ } else if (this._webRTCSession.direction === "outgoing") {
1358
+ this.log.debug("Call direction is OUTBOUND");
1359
+ return "OUTBOUND";
1360
+ } else {
1361
+ this.log.error("Unknown call direction: " + this._webRTCSession.direction);
1362
+ return "UNKNOWN";
1363
+ }
1364
+ }
1365
+
1366
+ // RECORDING
1367
+ /**
1368
+ * Start recording
1369
+ */
1370
+ startRecording() {
1371
+ const opt = {
1372
+ extraHeaders: ["Record: on"],
1373
+ };
1374
+
1375
+ this.log.debug("Request start recording");
1376
+
1377
+ const self = this;
1378
+ this._webRTCSession.once("newInfo", (data) => {
1379
+ if (data.originator === "local") {
1380
+ data.info.on("succeeded", function (/* data */) {
1381
+ self.log.info("Recording started successfully");
1382
+ self._isRecording = true;
1383
+ try {
1384
+ self.emit("recordingStateChange", { state: true, error: null });
1385
+ } catch (e) {
1386
+ self.log.error("Exception in recordingStateChange event: " + e);
1387
+ }
1388
+ });
1389
+
1390
+ data.info.on("failed", function (data) {
1391
+ self._recordingError = data.response.reason_phrase;
1392
+ self.log.error("Recording start failed: " + self._recordingError);
1393
+ self._fireRecordingStateEvent();
1394
+ });
1395
+ }
1396
+ });
1397
+
1398
+ this._webRTCSession.sendInfo("application/info", null, opt);
1399
+ }
1400
+
1401
+ /**
1402
+ * Stop recording
1403
+ */
1404
+ stopRecording() {
1405
+ const opt = {
1406
+ extraHeaders: ["Record: off"],
1407
+ };
1408
+
1409
+ this.log.debug("Request stop recording");
1410
+
1411
+ const self = this;
1412
+
1413
+ this._webRTCSession.once("newInfo", (data) => {
1414
+ data.info.on("succeeded", function (/* data */) {
1415
+ self.log.info("Recording stopped successfully");
1416
+ self._isRecording = false;
1417
+ try {
1418
+ self.emit("recordingStateChange", { state: false, error: null });
1419
+ } catch (e) {
1420
+ self.log.error("Exception in recordingStateChange event: " + e);
1421
+ }
1422
+ });
1423
+
1424
+ data.info.on("failed", function (data) {
1425
+ self.log.error("Recording stopped failed: " + data.response.reason_phrase);
1426
+ try {
1427
+ self.emit("recordingStateChange", { state: self._isRecording, error: data.response.reason_phrase });
1428
+ } catch (e) {
1429
+ self.log.error("Exception in recordingStateChange event: " + e);
1430
+ }
1431
+ });
1432
+ });
1433
+
1434
+ this._webRTCSession.sendInfo("application/info", null, opt);
1435
+ }
1436
+
1437
+ /**
1438
+ * Toogle recording ON/OFF
1439
+ */
1440
+ toggleRecording() {
1441
+ if (!this._isRecording) {
1442
+ this.startRecording();
1443
+ return true;
1444
+ } else {
1445
+ this.stopRecording();
1446
+ return false;
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Return recording state
1452
+ * @return {boolean}
1453
+ */
1454
+ get isRecording() {
1455
+ return this._isRecording;
1456
+ }
1457
+
1458
+ // MUTE
1459
+ /**
1460
+ * Mute microphone
1461
+ */
1462
+ muteMic() {
1463
+ this.log.info("Mute microphone");
1464
+ this._webRTCSession.mute();
1465
+ }
1466
+
1467
+ /**
1468
+ * Unmute microphone
1469
+ */
1470
+ unmuteMic() {
1471
+ this.log.info("Unmute microphone");
1472
+ this._webRTCSession.unmute();
1473
+ }
1474
+
1475
+ /**
1476
+ * Toggle microphone mute state
1477
+ */
1478
+ toggleMute() {
1479
+ if (!this._isMuted) {
1480
+ this.muteMic();
1481
+ } else {
1482
+ this.unmuteMic();
1483
+ }
1484
+ }
1485
+
1486
+ /**
1487
+ * Return mute state
1488
+ * @return {bool}
1489
+ */
1490
+ get isMuted() {
1491
+ return this._isMuted;
1492
+ }
1493
+
1494
+ // HOLD
1495
+ /**
1496
+ * Hold call
1497
+ */
1498
+ hold() {
1499
+ this.log.info("Hold call");
1500
+ this._webRTCSession.hold();
1501
+ }
1502
+
1503
+ /**
1504
+ * Unhold call
1505
+ */
1506
+ unhold() {
1507
+ this.log.info("Uncall call");
1508
+ this._phone.holdAllCalls();
1509
+ this._webRTCSession.unhold();
1510
+ }
1511
+
1512
+ /**
1513
+ * Toggle call hold
1514
+ */
1515
+ toggleHold() {
1516
+ if (!this._isOnHold) {
1517
+ this.hold();
1518
+ } else {
1519
+ this.unhold();
1520
+ }
1521
+ }
1522
+
1523
+ /**
1524
+ * Return call hold status
1525
+ * @return {bool}
1526
+ */
1527
+ get isOnHold() {
1528
+ return this._isOnHold;
1529
+ }
1530
+
1531
+ /**
1532
+ * Send DTMF tone on call
1533
+ * @param code What DTMF code to send (valid: 0-9,*,#,d)
1534
+ * @param playLocal=True Play DTMF sound locally also
1535
+ * @throws InvalidParameter - On incorrect DTMF codes
1536
+ */
1537
+ sendDTMF(code, playLocal = true) {
1538
+ // Check for correct DTMF and get frequencies
1539
+ if (!(code in DTMF_FREQUENCY_TABLE)) {
1540
+ throw new Exceptions.InvalidParameter("Invalid DTMF code");
1541
+ }
1542
+
1543
+ this.log.info("Sending DTMF: " + code);
1544
+ this._webRTCSession.sendDTMF(code);
1545
+
1546
+ if (playLocal) {
1547
+ const f = DTMF_FREQUENCY_TABLE[code];
1548
+
1549
+ // First mute microphone before playing DTMF
1550
+ if (this._isMuted) {
1551
+ this.log.debug("Mic already muted, just sending DTMF");
1552
+ this._phone.playTone(500, f[0], f[1]);
1553
+ } else {
1554
+ this.log.debug("Mute mic while sending DTMF");
1555
+ this._webRTCSession.mute();
1556
+
1557
+ const self = this;
1558
+ this._phone.playTone(500, f[0], f[1], function () {
1559
+ self.log.debug("Unmute mic after DTMF");
1560
+ self._webRTCSession.unmute();
1561
+ });
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ /**
1567
+ * Transfer call to another session (attended transfer)
1568
+ *
1569
+ * @param dstSession Destination session
1570
+ */
1571
+ transfer(dstSession) {
1572
+ this.log.info(`Transferring ${this.uuid} => ${dstSession.uuid}`);
1573
+
1574
+ // Create ReferSubscriber event class
1575
+ const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, { replaces: dstSession._webRTCSession });
1576
+
1577
+ const self = this;
1578
+
1579
+ // Setup some logging
1580
+ referSubscriber.on("requestSucceeded", function () {
1581
+ self.log.info("Transfer completed successfully");
1582
+ });
1583
+
1584
+ referSubscriber.on("requestFailed", function (cause) {
1585
+ self.log.error(`Transfer rejected: ${cause}`);
1586
+ });
1587
+
1588
+ referSubscriber.on("failed", function (cause) {
1589
+ self.log.error(`Transfer failed: ${cause}`);
1590
+ });
1591
+ }
1592
+
1593
+ /**
1594
+ * Playback media file into call. You must be connected the event service to use this feature (set connectEventService=true)
1595
+ *
1596
+ * @param media_file_id Media file to playback
1597
+ * @param hangup_after Hangup call after playback ends
1598
+ *
1599
+ * @throws NotAllowed - If event service is not enabled
1600
+ *
1601
+ * @return Promise that resolves when playback has started
1602
+ */
1603
+ playback(media_file_id, hangup_after = false) {
1604
+ // Check event service is enabled first
1605
+ if (!this._phone._options.connectEventService) {
1606
+ throw new Exceptions.NotAllowed("Event service not enabled");
1607
+ }
1608
+
1609
+ this.log.info(`Playback media file ${media_file_id} hangup after ${hangup_after}`);
1610
+
1611
+ const p = this._phone._eventClient.fireEvent("call_command", {
1612
+ call_uuid: this.uuid,
1613
+ call_command: "playback",
1614
+ media_file_id: media_file_id,
1615
+ hangup_after: hangup_after,
1616
+ });
1617
+
1618
+ // When playback is acknowledged and 'hangup_after' is true, we set session state into PLAYBACK
1619
+ if (hangup_after) {
1620
+ // Stop remote audio
1621
+ this._phone._remoteAudioElement.srcObject = null;
1622
+
1623
+ // Mute microphone
1624
+ this._webRTCSession.mute();
1625
+
1626
+ // When playback starts set state to playback
1627
+ p.then(() => {
1628
+ // Set session state
1629
+ this._setSessionState("PLAYBACK");
1630
+ });
1631
+ }
1632
+
1633
+ return p;
1634
+ }
1635
+
1636
+ /**
1637
+ * Set whisper mode on "listen" calls
1638
+ *
1639
+ * @param mode Whisper mode 0 = No whisper / 1 = local peer only / 2 = Three way (both local and remote)
1640
+ *
1641
+ * @throws NotAllowed - If call is not listen call
1642
+ * @throws InvalidOptions - If invalid mode given
1643
+ */
1644
+ setWhisperMode(mode = 0) {
1645
+ if (!this.direction === "LISTEN") {
1646
+ throw new Exceptions.NotAllowed("Call is not correct type (listen)");
1647
+ }
1648
+
1649
+ if (mode &lt; 0 || mode > 2) {
1650
+ throw new Exceptions.InvalidOptions("Invalid mode");
1651
+ }
1652
+
1653
+ let t_dtmf_mode;
1654
+
1655
+ if (mode === 0) {
1656
+ t_dtmf_mode = 0;
1657
+ }
1658
+
1659
+ if (mode === 1) {
1660
+ t_dtmf_mode = 2;
1661
+ }
1662
+
1663
+ if (mode === 2) {
1664
+ t_dtmf_mode = 3;
1665
+ }
1666
+
1667
+ this.sendDTMF(t_dtmf_mode, false);
1668
+ this._whisperMode = mode;
1669
+ this._fireWhisperModeChangeEvent();
1670
+ }
1671
+
1672
+ /*
1673
+ * ======= PRIVATE METHODS PHONE SESSION =========================================================================
1674
+ */
1675
+
1676
+ // Begin update timer events
1677
+ _startDurationUpdateTimer() {
1678
+ /**
1679
+ * @event callDurationUpdate
1680
+ * @type {object}
1681
+ * @property {String} uuid - UUID of session
1682
+ * @property {Date} beginTime - Time session was created
1683
+ * @property {Date} answerTime - Time session was answered
1684
+ * @property {Date} duration - The total duration of the session
1685
+ * @property {String} durationStr - The duration of the session string formated (mm:ss)
1686
+ * @property {Date} activeDuration - The active duration of the session
1687
+ * @property {String} activeDurationStr - The active duration of the session string formated (mm:ss)
1688
+ *
1689
+ * @example
1690
+ * // Update duration text on screen
1691
+ * session.on('callDurationUpdate', function (event) {
1692
+ * document.getElementById('duration').textContent = event.durationStr;
1693
+ * });
1694
+ */
1695
+
1696
+ const self = this;
1697
+ if (!this._durationUpdTimerObj) {
1698
+ this._durationUpdTimerObj = setInterval(function () {
1699
+ const activeDuration = self._callAnswerTime ? Date.now() - self._callAnswerTime : null;
1700
+ const e = {
1701
+ uuid: self._callUUID,
1702
+ beginTime: self._callBeginTime,
1703
+ answerTime: self._callAnswerTime,
1704
+ endTime: self._callEndTime,
1705
+ duration: Date.now() - self._callBeginTime,
1706
+ durationStr: Tools.createDurationString(Date.now() - self._callBeginTime),
1707
+ activeDuration: activeDuration,
1708
+ activeDurationStr: activeDuration ? Tools.createDurationString(activeDuration) : "",
1709
+ };
1710
+
1711
+ try {
1712
+ self.emit("callDurationUpdate", e);
1713
+ } catch (e) {
1714
+ self.log.error("Exception in call duration update event: " + e);
1715
+ }
1716
+ }, 1000);
1717
+ }
1718
+ }
1719
+
1720
+ // Begin rtcp stats update timer events
1721
+ _startRTCPStatsUpdateTimer() {
1722
+ /**
1723
+ * @event callQualityReportUpdate
1724
+ * @description
1725
+ * Contains information about call quality (fires in interval of 5 sec).
1726
+ * Payload contains "total" (metrics for whole session) and "delta" (metrics last 5 secs).
1727
+ *
1728
+ * @type {object}
1729
+ * @property {String} uuid - UUID of session
1730
+ * @property {Number} outboundPacketsSent - Number of packets sent
1731
+ * @property {Number} outboundBytesSent - Number of bytes sent
1732
+ * @property {Number} outboundPacketsLost - Number of packets lost to remote
1733
+ * @property {Number} outboundPacketLossPct - Percentage of outbound packets lost
1734
+ * @property {Number} outboundJitter - Network jitter to remote (ms)
1735
+ * @property {Number} inboundPacketsReceived - Number of packets received
1736
+ * @property {Number} inboundBytesReceived - Number of bytes received
1737
+ * @property {Number} inboundPacketsLost - Number of packets lost from remote
1738
+ * @property {Number} inboundPacketLossPct - Percentage of inbound packets lost
1739
+ * @property {Number} inboundJitter - Network jitter from remote (ms)
1740
+ * @property {Number} roundTripTime - Latency in ms to media gateway
1741
+ * @property {Number} effectiveLatency - Effective latency with combined jitter impact
1742
+ * @property {Number} rFactor - Quality of media stream (0 - 100%)
1743
+ * @property {Number} mos - Synthetic MOS
1744
+ * @property {String} qualityString - Quality string [Excellent, Good, Fair, Poor, Bad]
1745
+ *
1746
+ * @example
1747
+ * // Update call quality text on screen
1748
+ * session.on('callQualityReportUpdate', (report) => {
1749
+ * document.getElementById('quality_text').textContent = report.delta.qualityString;
1750
+ * document.getElementById('latency').textContent = report.delta.effectiveLatency;
1751
+ * });
1752
+ */
1753
+
1754
+ if (!this._rtcpStatsTimer) {
1755
+ const emptyReport = {
1756
+ outboundPacketsSent: 0,
1757
+ outboundBytesSent: 0,
1758
+ outboundPacketsLost: 0,
1759
+ outboundJitter: 0,
1760
+ inboundPacketsReceived: 0,
1761
+ inboundBytesReceived: 0,
1762
+ inboundPacketsLost: 0,
1763
+ inboundJitter: 0,
1764
+ roundTripTime: 0,
1765
+ };
1766
+
1767
+ // Initialize report first
1768
+ this._currentReport = Object.assign({}, emptyReport);
1769
+
1770
+ this._rtcpStatsTimer = setInterval(() => {
1771
+ this._webRTCSession._connection.getStats().then((data) => {
1772
+ let totalReport = Object.assign({}, emptyReport);
1773
+ let deltaReport = Object.assign({}, emptyReport);
1774
+
1775
+ for (let report of data.values()) {
1776
+ // this.log.debug(report);
1777
+ switch (report.type) {
1778
+ case "outbound-rtp":
1779
+ // this.log.debug('Got: outbound-rtp');
1780
+ // this.log.debug(report);
1781
+ // Packets
1782
+ totalReport.outboundPacketsSent = report.packetsSent;
1783
+ deltaReport.outboundPacketsSent = report.packetsSent - this._currentReport.outboundPacketsSent;
1784
+ // Bytes
1785
+ totalReport.outboundBytesSent = report.bytesSent;
1786
+ deltaReport.outboundBytesSent = report.bytesSent - this._currentReport.outboundBytesSent;
1787
+ break;
1788
+
1789
+ case "inbound-rtp":
1790
+ // this.log.debug('Got: inbound-rtp');
1791
+ // this.log.debug(report);
1792
+ // Packets
1793
+ totalReport.inboundPacketsReceived = report.packetsReceived;
1794
+ deltaReport.inboundPacketsReceived = report.packetsReceived - this._currentReport.inboundPacketsReceived;
1795
+ // Bytes
1796
+ totalReport.inboundBytesReceived = report.bytesReceived;
1797
+ deltaReport.inboundBytesReceived = report.bytesReceived - this._currentReport.inboundBytesReceived;
1798
+ // Loss
1799
+ totalReport.inboundPacketsLost = report.packetsLost;
1800
+ deltaReport.inboundPacketsLost = report.packetsLost - this._currentReport.inboundPacketsLost;
1801
+ // Jitter
1802
+ totalReport.inboundJitter = report.jitter * 1000;
1803
+ deltaReport.inboundJitter = report.jitter * 1000;
1804
+ break;
1805
+
1806
+ case "remote-inbound-rtp":
1807
+ // this.log.debug('Got: remote-inbound-rtp');
1808
+ // this.log.debug(report);
1809
+ // Loss
1810
+ totalReport.outboundPacketsLost = report.packetsLost;
1811
+ deltaReport.outboundPacketsLost = report.packetsLost - this._currentReport.outboundPacketsLost;
1812
+ // Jitter
1813
+ totalReport.outboundJitter = report.jitter * 1000;
1814
+ deltaReport.outboundJitter = report.jitter * 1000;
1815
+ // RTT
1816
+ totalReport.roundTripTime = report.roundTripTime * 1000;
1817
+ deltaReport.roundTripTime = report.roundTripTime * 1000;
1818
+ break;
1819
+
1820
+ default:
1821
+ // this.log.debug(`Report ${report.type} ignored`);
1822
+ break;
1823
+ }
1824
+ }
1825
+
1826
+ this._currentReport = this._calcReportMetrics(totalReport);
1827
+
1828
+ // We have to wait for RTT before firing the first event
1829
+ if (totalReport.roundTripTime) {
1830
+ // Do current report metrics
1831
+ const finalReport = {
1832
+ total: this._currentReport,
1833
+ delta: this._calcReportMetrics(deltaReport),
1834
+ };
1835
+
1836
+ try {
1837
+ this.emit("callQualityReportUpdate", finalReport);
1838
+ } catch (e) {
1839
+ this.log.error("Exception in call quality report update event: " + e);
1840
+ }
1841
+ }
1842
+ });
1843
+ }, 5000);
1844
+ }
1845
+ }
1846
+
1847
+ // Return quality string based on R-factor
1848
+ _calcReportMetrics(report) {
1849
+ // Packet loss
1850
+ report.outboundPacketLossPct = report.outboundPacketsLost / report.outboundPacketsSent;
1851
+ report.inboundPacketLossPct = report.inboundPacketsLost / report.inboundPacketsReceived;
1852
+
1853
+ // Effective latency
1854
+ report.effectiveLatency = report.roundTripTime + (report.inboundJitter + report.outboundJitter) + 10;
1855
+
1856
+ // R-Factor and mos
1857
+ const lossRate = (report.outboundPacketLossPct + report.inboundPacketLossPct) * 100;
1858
+ const lossEffect = (95.0 * lossRate) / (lossRate + 10);
1859
+ const latencyEffect = report.effectiveLatency * 0.1;
1860
+ // this.log.debug(`R factor loss effect ${lossEffect} / latency effect ${latencyEffect}`);
1861
+
1862
+ report.rFactor = Math.max(0, 100 - lossEffect - latencyEffect);
1863
+ report.mos = 1 + 0.035 * report.rFactor + 0.000007 * report.rFactor * (report.rFactor - 60) * (100 - report.rFactor);
1864
+
1865
+ // Quality string
1866
+ switch (true) {
1867
+ case report.rFactor &lt; 50:
1868
+ report.qualityString = "Bad";
1869
+ break;
1870
+ case report.rFactor &lt; 70:
1871
+ report.qualityString = "Poor";
1872
+ break;
1873
+ case report.rFactor &lt; 80:
1874
+ report.qualityString = "Fair";
1875
+ break;
1876
+ case report.rFactor &lt; 90:
1877
+ report.qualityString = "Good";
1878
+ break;
1879
+ case report.rFactor &lt;= 100:
1880
+ report.qualityString = "Excellent";
1881
+ break;
1882
+ default:
1883
+ report.qualityString = "Error";
1884
+ }
1885
+
1886
+ return report;
1887
+ }
1888
+
1889
+ /**
1890
+ * @typedef SESSION_STATE {string}
1891
+ * @property {string} INITIALIZING Session is initializing (Gather ICE candidates)
1892
+ * @property {string} CALLING Sending INVITE to server
1893
+ * @property {string} PROGRESS Call progress received ('event.code' has new progress status)
1894
+ * @property {string} ACTIVE Call is active (answered)
1895
+ * @property {string} TERMINATED Call ended
1896
+ * @property {string} FAILED Call failed (see 'event.cause' for more info)
1897
+ * @property {string} ON_HOLD Call is on-hold
1898
+ * @property {string} INCOMING Incoming call is waiting
1899
+ * @property {string} PLAYBACK Playback with pending hangup in progress
1900
+ */
1901
+
1902
+ // Set session state and emit event
1903
+ _setSessionState(state, info = null) {
1904
+ /**
1905
+ * @event sessionStateChange
1906
+ * @type {object}
1907
+ * @param {SESSION_STATE} state - The current session state
1908
+ * @param {uuid} uuid - Call UUID for session
1909
+ * @param {string} originName - Originating name of call [INCOMING only]
1910
+ * @param {string} originatingNumber - Originating phone number of call [INCOMING only]
1911
+ * @param {string} destinationNumber - Destination number of call
1912
+ * @param {string} cause - Fail cause [FAILED only]
1913
+ * @param {string} message - Fail message description [FAILED only]
1914
+ *
1915
+ */
1916
+
1917
+ this.log.debug("Session state: " + state);
1918
+ if (info) {
1919
+ this.log.debug(info);
1920
+ }
1921
+
1922
+ this._sessionState = state;
1923
+ try {
1924
+ this.emit(
1925
+ "sessionStateChange",
1926
+ Object.assign(
1927
+ {
1928
+ state: state,
1929
+ uuid: this._callUUID,
1930
+ },
1931
+ info,
1932
+ ),
1933
+ );
1934
+ } catch (e) {
1935
+ this.log.error("Exception in sessionStateChange event:" + e);
1936
+ }
1937
+
1938
+ // Trigger callDataAvailable dummy event
1939
+ if (state === "TERMINATED") {
1940
+ this._callDisposition = this._manualDisposition || "NORMAL";
1941
+ this._callFinished();
1942
+ }
1943
+
1944
+ if (state === "FAILED") {
1945
+ if (info.cause === "Unavailable") {
1946
+ this._callDisposition = "TEMP_UNAVAIL";
1947
+ if (this._phone.options.playSignalTones) {
1948
+ this._phone._startRepeatTone(ST_TEMP_UNAVAIL);
1949
+ }
1950
+ }
1951
+
1952
+ if (info.cause === "Not Found") {
1953
+ this._callDisposition = "INVALID_NUMBER";
1954
+ if (this._phone.options.playSignalTones) {
1955
+ this._phone._startRepeatTone(ST_INVALID_NUMBER);
1956
+ }
1957
+ }
1958
+
1959
+ if (info.cause === "Canceled") {
1960
+ this._callDisposition = this._manualDisposition || "NO_ANSWER";
1961
+ }
1962
+
1963
+ if (info.cause === "Rejected") {
1964
+ if (this.direction === "INBOUND") {
1965
+ this._callDisposition = "REJECTED";
1966
+ } else {
1967
+ this._callDisposition = "BARRED";
1968
+ }
1969
+ }
1970
+
1971
+ if (info.cause === "Busy") {
1972
+ this._callDisposition = "BUSY";
1973
+ if (this._phone.options.playSignalTones) {
1974
+ this._phone._startRepeatTone(ST_BUSY);
1975
+ }
1976
+ }
1977
+
1978
+ // If session is incoming and failed, we don't fire calldataAvailable event
1979
+ if (this._webRTCSession.direction === "incoming") {
1980
+ // return;
1981
+ }
1982
+
1983
+ this._callFinished();
1984
+ }
1985
+ }
1986
+
1987
+ /**
1988
+ * @typedef {string} CALL_DIRECTION
1989
+ * @property {string} INBOUND Incoming call
1990
+ * @property {string} OUTBOUND Outgoing call
1991
+ * @property {string} TRANSFER Transferred call
1992
+ * @property {string} LOCAL Local call
1993
+ * @property {string} AUTOMATED Call triggered from api / server
1994
+ * @property {string} LISTEN Listen call
1995
+ */
1996
+
1997
+ /**
1998
+ * @typedef {string} CALL_DISPOSITION
1999
+ * @property {string} BUSY Number was busy
2000
+ * @property {string} INVALID_NUMBER Number call does not exist
2001
+ * @property {string} NO_ANSWER Call was not picked up before cancelling
2002
+ * @property {string} TEMP_UNAVAIL Remote is temporarily unavailable
2003
+ * @property {string} NORMAL Call was terminated normally
2004
+ * @property {string} FAILED There was a failure connecting the call
2005
+ * @property {string} BARRED Call was prohibited by server
2006
+ * @property {string} REJECTED Incoming call was rejected
2007
+ */
2008
+ _callFinished() {
2009
+ this.log.info("Call finished");
2010
+
2011
+ // Fire callEnded event
2012
+ this._fireCallEndedEvent();
2013
+
2014
+ // Stop duration update timer
2015
+ clearInterval(this._durationUpdTimerObj);
2016
+
2017
+ // Stop rtcp stats uptime timer
2018
+ clearInterval(this._rtcpStatsTimer);
2019
+
2020
+ // Remove session from active sessions
2021
+ this._phone._sessionTerminated(this);
2022
+ }
2023
+
2024
+ // Set answer call timeout
2025
+ _setAnswerCallTimeout(timeout) {
2026
+ this.log.debug("Setting answer call timeout to: " + timeout);
2027
+ this._answerCallTimeout = timeout;
2028
+ }
2029
+
2030
+ // Fire recording state event
2031
+ _fireRecordingStateEvent() {
2032
+ /**
2033
+ * @event recordingStateChange
2034
+ * @type {object}
2035
+ * @property {boolean} state - New recording state
2036
+ */
2037
+ try {
2038
+ this.emit("recordingStateChange", { state: this._isRecording, error: this._recordingError });
2039
+ } catch (e) {
2040
+ this.log.error("Exception in recording state change event: " + e);
2041
+ }
2042
+ }
2043
+
2044
+ // Fire mic mute state event
2045
+ _fireMuteStateEvent() {
2046
+ /**
2047
+ * @event muteStateChange
2048
+ * @type {object}
2049
+ * @property {boolean} state - Mute state (true = muted / false = unmuted)
2050
+ */
2051
+ try {
2052
+ this.emit("muteStateChange", { state: this._isMuted });
2053
+ } catch (e) {
2054
+ this.log.error("Exception in mute state change event: " + e);
2055
+ }
2056
+ }
2057
+
2058
+ // Fire recording state event
2059
+ _fireHoldStateEvent() {
2060
+ /**
2061
+ * @event holdStateChange
2062
+ * @type {object}
2063
+ * @property {boolean} state - New hold state
2064
+ */
2065
+ try {
2066
+ this.emit("holdStateChange", { state: this._isOnHold });
2067
+ } catch (e) {
2068
+ this.log.error("Exception in hold state change event: " + e);
2069
+ }
2070
+ }
2071
+
2072
+ // Fire whisper mode event
2073
+ _fireWhisperModeChangeEvent() {
2074
+ /**
2075
+ * @event whisperModeChange
2076
+ * @type {object}
2077
+ * @property {boolean} mode - New whisper mode (0 = No whisper / 1 = Local only / 2 = Three way)
2078
+ */
2079
+ try {
2080
+ this.emit("whisperModeChange", { mode: this._whisperMode });
2081
+ } catch (e) {
2082
+ this.log.error("Exception in whisper mode change event: " + e);
2083
+ }
2084
+ }
2085
+
2086
+ // Fire call data event to subscribers
2087
+ _fireCallEndedEvent() {
2088
+ /**
2089
+ * @event callEnded
2090
+ *
2091
+ * @type {object}
2092
+ * @property {uuid} uuid - Customer Call UUID for session
2093
+ * @param {CALL_DIRECTION} direction - Call direction
2094
+ * @param {CALL_DISPOSITION} disposition - The final call disposition
2095
+ * @param {string} originatingNumber - Caller ID number (a-number) used for call
2096
+ * @param {string} destinationNumber - Destination number (b-number) used for call
2097
+ * @param {Date} startTime - Start time of call
2098
+ * @param {Date} answerTime - Time call was answered
2099
+ * @param {Date} endTime - End time of call
2100
+ * @param {number} duration - Total duration of the call (including call setup and remote answer time) (millisec)
2101
+ * @param {String} durationStr - Total duration of the call formatted as (mm:ss)
2102
+ * @param {Number} answerDuration - Time before call was answered (millisec)
2103
+ * @param {String} answerDurationStr - Time before call was answered formatted as (mm:ss)
2104
+ * @param {Number} activeDuration - Active duration of the call (time spend talking) (millisec)
2105
+ * @param {String} activeDurationStr - Active duration of the call formatted as (mm:ss)
2106
+ * @param {object} callQualityReport - Call quality report (see callQualityReportUpdate event for details)
2107
+ */
2108
+
2109
+ let e;
2110
+
2111
+ e = {
2112
+ uuid: this._callUUID,
2113
+ direction: this.direction,
2114
+ disposition: this._callDisposition,
2115
+ originatingNumber: this._originatingNumber,
2116
+ destinationNumber: this._destinationNumber,
2117
+
2118
+ // Calc durations
2119
+ duration: this._callEndTime - this._callBeginTime,
2120
+ answerDuration: this._callAnswerTime ? this._callAnswerTime - this._callBeginTime : null,
2121
+ activeDuration: this._callAnswerTime ? this._callEndTime - this._callAnswerTime : null,
2122
+
2123
+ // Convert timestamp into ISO8601
2124
+ startTime: new Date(this._callBeginTime).toISOString(),
2125
+ endTime: new Date(this._callEndTime).toISOString(),
2126
+ answerTime: this._callAnswerTime ? new Date(this._callAnswerTime).toISOString() : null,
2127
+ };
2128
+
2129
+ // Add some extra convenience fields
2130
+ e.durationStr = Tools.createDurationString(e.duration);
2131
+ e.answerDurationStr = e.answerDuration ? Tools.createDurationString(e.answerDuration) : "";
2132
+ e.activeDurationStr = e.activeDuration ? Tools.createDurationString(e.activeDuration) : "";
2133
+ e.callQualityReport = this._currentReport;
2134
+
2135
+ try {
2136
+ this.emit("callEnded", e);
2137
+ } catch (e) {
2138
+ this.log.error("Exception in callEnded event: " + e);
2139
+ }
2140
+ }
2141
+
2142
+ // Setup handling of remote audio
2143
+ _handleRemoteAudio(session) {
2144
+ // Stop any repeating tones if audio comes
2145
+ this._phone._endRepeatTone();
2146
+
2147
+ const self = this;
2148
+ session.connection.ontrack = function (event) {
2149
+ self.log.debug("Start media output (track):");
2150
+ self.log.debug(event);
2151
+ self._phone._remoteAudioElement.srcObject = event.streams[0];
2152
+ };
2153
+ }
2154
+ }
2155
+ </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="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>