@telegenta/webclient 3.1.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/telegenta.js +178 -39
- package/dist/telegenta.min.js +1 -1
- package/docs/Phone.html +1 -1
- package/docs/Session.html +2 -2
- package/docs/data/search.json +1 -1
- package/docs/event-client.js.html +4 -4
- package/docs/global.html +3 -3
- package/docs/phone-client.js.html +190 -51
- package/docs/requestor.js.html +6 -6
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@ A browser client library for using Telegenta phone services
|
|
|
4
4
|
|
|
5
5
|
# Detailed documentation
|
|
6
6
|
|
|
7
|
-
Please go to: https://www.telegenta.com/docs/index.html
|
|
7
|
+
Please go to: https://www.telegenta.com/docs/webclient/latest/index.html
|
|
8
|
+
|
|
9
|
+
Documentation for a specific version is at `https://www.telegenta.com/docs/webclient/<version>/index.html`,
|
|
10
|
+
e.g. https://www.telegenta.com/docs/webclient/3.2.0/index.html
|
|
8
11
|
|
|
9
12
|
## Why this library?
|
|
10
13
|
|
|
@@ -142,6 +145,33 @@ If you are migrating from 1.7.x release into 2.x, some code breaking changes nee
|
|
|
142
145
|
|
|
143
146
|
This library uses the JsSIP library (www.jssip.net) created by José Luis Milàn, Iñaki Baz Castillo and Saúl Ibarra Corretgé
|
|
144
147
|
|
|
148
|
+
## Changelog
|
|
149
|
+
|
|
150
|
+
3.2.0
|
|
151
|
+
- Phone.setCallToken(token): replace the call token without restarting (tokens expire after 30 min).
|
|
152
|
+
With useCallDataInfoEvent, the callDataInfo subscription keeps the token from start(); call
|
|
153
|
+
start() again to move it to the new token.
|
|
154
|
+
- Phone.prepareMedia({ deviceId }) and Phone.setInputDevice(deviceId): microphone selection
|
|
155
|
+
- Phone.prepareMedia() without a deviceId resets a microphone previously chosen with setInputDevice()
|
|
156
|
+
- Phone.setOutputDevice(deviceId) and Phone.outputDeviceSelectionSupported: speaker selection
|
|
157
|
+
- prepareMedia() releases the microphone after the permission probe
|
|
158
|
+
- Phone.resumeAudio(): resumes the signal-tone AudioContext; call it from a user-gesture handler,
|
|
159
|
+
since a phone started on page load gets a "suspended" context and plays no tones. Tones also
|
|
160
|
+
try to resume it themselves. One AudioContext per Phone is now reused across start()/stop()
|
|
161
|
+
(previously every start() created a new one that was never closed)
|
|
162
|
+
- prepareMedia() now rejects instead of throwing synchronously, and a failed request no longer
|
|
163
|
+
changes the selected microphone or media-permission state unless the browser actually denied
|
|
164
|
+
permission (NotAllowedError/SecurityError)
|
|
165
|
+
- start() no longer mutates the shared default options between Phone instances
|
|
166
|
+
- stop() now always marks the phone stopped
|
|
167
|
+
- Phone.register(): re-sends REGISTER without restarting the phone or dropping active calls
|
|
168
|
+
(throws NotReady if not started) - for recovering a REGISTER refresh that failed for a reason
|
|
169
|
+
other than bad credentials, which JsSIP does not retry on its own
|
|
170
|
+
- connectionStateChange now carries `cause` and `statusCode` alongside `state` when
|
|
171
|
+
state is REGISTRATION_FAILED (from JsSIP's registrationFailed event), so callers can tell a
|
|
172
|
+
transient network failure (JsSIP retries on its own) apart from an authentication/authorization
|
|
173
|
+
failure (401/403) that needs a fresh call session
|
|
174
|
+
|
|
145
175
|
#### License
|
|
146
176
|
|
|
147
177
|
This software is provided for free together with our service
|
package/dist/telegenta.js
CHANGED
|
@@ -1643,6 +1643,9 @@ class Phone extends EventEmitter {
|
|
|
1643
1643
|
this._eventClient = null;
|
|
1644
1644
|
this._options = null;
|
|
1645
1645
|
|
|
1646
|
+
// getUserMedia audio constraint: true (default device) or { deviceId: { exact } }
|
|
1647
|
+
this._audioConstraints = true;
|
|
1648
|
+
|
|
1646
1649
|
// List of active sessions
|
|
1647
1650
|
this._sessionCounter = 0;
|
|
1648
1651
|
this._activeSessions = {};
|
|
@@ -1658,6 +1661,7 @@ class Phone extends EventEmitter {
|
|
|
1658
1661
|
this._repeatToneActive = false;
|
|
1659
1662
|
this._repeatToneSequence = [];
|
|
1660
1663
|
this._repeatToneSequencePos = 0;
|
|
1664
|
+
this._toneContext = null;
|
|
1661
1665
|
this._toneGainProcessor = null;
|
|
1662
1666
|
this._oscillator1 = null;
|
|
1663
1667
|
this._oscillator2 = null;
|
|
@@ -1677,29 +1681,83 @@ class Phone extends EventEmitter {
|
|
|
1677
1681
|
* Prepare media and get media permission from browser
|
|
1678
1682
|
* This must be done prior to using the phone or getting device lists.
|
|
1679
1683
|
*
|
|
1680
|
-
*
|
|
1684
|
+
* On failure the previously selected input device (see {@link setInputDevice}) and the
|
|
1685
|
+
* current media-permission state are left untouched, unless the browser reports the
|
|
1686
|
+
* request was actually denied (NotAllowedError / SecurityError), in which case
|
|
1687
|
+
* hasMediaPermission/hasMediaPermissionError are updated to reflect that.
|
|
1681
1688
|
*
|
|
1682
|
-
|
|
1683
|
-
|
|
1689
|
+
* @param {object} [mediaOptions]
|
|
1690
|
+
* @param {string} [mediaOptions.deviceId] Microphone deviceId (from enumerateDevices); omit or leave
|
|
1691
|
+
* out to select the browser default device, which also resets a device previously chosen with
|
|
1692
|
+
* {@link setInputDevice}
|
|
1693
|
+
* @return {Promise} Resolved when user accepts access to media devices; rejected (never thrown
|
|
1694
|
+
* synchronously) otherwise, e.g. when the browser denies permission or has no mediaDevices API
|
|
1695
|
+
*/
|
|
1696
|
+
async prepareMedia(mediaOptions = {}) {
|
|
1684
1697
|
this.log.info('Get media permission');
|
|
1685
|
-
const
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1698
|
+
const deviceId = (mediaOptions || {}).deviceId || null;
|
|
1699
|
+
const audioConstraints = deviceId ? { deviceId: { exact: deviceId } } : true;
|
|
1700
|
+
const constraints = { audio: audioConstraints, video: false };
|
|
1701
|
+
|
|
1702
|
+
try {
|
|
1703
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
1704
|
+
// Only the permission is needed here; release the device again.
|
|
1705
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
1706
|
+
this.setInputDevice(deviceId);
|
|
1707
|
+
this._mediaPermission = true;
|
|
1708
|
+
this._mediaPermissionError = false;
|
|
1709
|
+
this.log.info('Media permission granted');
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
if (err && (err.name === 'NotAllowedError' || err.name === 'SecurityError')) {
|
|
1712
|
+
this._mediaPermission = false;
|
|
1713
|
+
this._mediaPermissionError = true;
|
|
1714
|
+
this.log.error('Denied media permission: ' + err);
|
|
1715
|
+
} else {
|
|
1716
|
+
this.log.error('Unable to get requested media device: ' + err);
|
|
1717
|
+
}
|
|
1718
|
+
throw err;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Select the microphone used for subsequent calls and answers.
|
|
1724
|
+
* @param {string|null} deviceId Microphone deviceId, or null for the browser default
|
|
1725
|
+
*/
|
|
1726
|
+
setInputDevice(deviceId) {
|
|
1727
|
+
this._audioConstraints = deviceId ? { deviceId: { exact: deviceId } } : true;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Current getUserMedia audio constraint (true = default device)
|
|
1732
|
+
*/
|
|
1733
|
+
get audioConstraints() {
|
|
1734
|
+
return this._audioConstraints;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* True when the browser can route audio to a chosen output device (HTMLMediaElement.setSinkId)
|
|
1739
|
+
*/
|
|
1740
|
+
get outputDeviceSelectionSupported() {
|
|
1741
|
+
return typeof this._remoteAudioElement.setSinkId === 'function';
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Route call audio and signal tones to an output device.
|
|
1746
|
+
* @param {string|null} deviceId Output deviceId (from enumerateDevices), or null for the default
|
|
1747
|
+
* @return {Promise<boolean>} false when the browser does not support output selection
|
|
1748
|
+
* @throws Rejects if the browser's HTMLMediaElement.setSinkId() call rejects, e.g. for an
|
|
1749
|
+
* unknown deviceId or one the browser is not allowed to use (NotAllowedError)
|
|
1750
|
+
*/
|
|
1751
|
+
async setOutputDevice(deviceId) {
|
|
1752
|
+
if (!this.outputDeviceSelectionSupported) {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
const sinkId = deviceId || '';
|
|
1756
|
+
await Promise.all([
|
|
1757
|
+
this._remoteAudioElement.setSinkId(sinkId),
|
|
1758
|
+
this._toneAudioElement.setSinkId(sinkId),
|
|
1759
|
+
]);
|
|
1760
|
+
return true;
|
|
1703
1761
|
}
|
|
1704
1762
|
|
|
1705
1763
|
/**
|
|
@@ -1735,7 +1793,7 @@ class Phone extends EventEmitter {
|
|
|
1735
1793
|
}
|
|
1736
1794
|
|
|
1737
1795
|
// Apply options
|
|
1738
|
-
this._options = Object.assign(options, phoneOptions);
|
|
1796
|
+
this._options = Object.assign({}, options, phoneOptions);
|
|
1739
1797
|
|
|
1740
1798
|
this._initToneGenerator();
|
|
1741
1799
|
|
|
@@ -1898,8 +1956,11 @@ class Phone extends EventEmitter {
|
|
|
1898
1956
|
self._setConnectionState('UNREGISTERED');
|
|
1899
1957
|
});
|
|
1900
1958
|
|
|
1901
|
-
this._UA.on('registrationFailed', function (
|
|
1902
|
-
self._setConnectionState('REGISTRATION_FAILED'
|
|
1959
|
+
this._UA.on('registrationFailed', function (data) {
|
|
1960
|
+
self._setConnectionState('REGISTRATION_FAILED', {
|
|
1961
|
+
cause: data?.cause,
|
|
1962
|
+
statusCode: data?.response?.status_code,
|
|
1963
|
+
});
|
|
1903
1964
|
});
|
|
1904
1965
|
|
|
1905
1966
|
this._UA.start();
|
|
@@ -1924,6 +1985,11 @@ class Phone extends EventEmitter {
|
|
|
1924
1985
|
this._UA.stop();
|
|
1925
1986
|
delete this._UA;
|
|
1926
1987
|
|
|
1988
|
+
// Always mark the phone stopped from here on, even if there is no event client to
|
|
1989
|
+
// disconnect below - otherwise isStarted stays true and a later start() would try to
|
|
1990
|
+
// stop() this same (already torn down) instance again.
|
|
1991
|
+
this._isStarted = false;
|
|
1992
|
+
|
|
1927
1993
|
// Disconnect event client if needed
|
|
1928
1994
|
if (!this._eventClient) {
|
|
1929
1995
|
this.log.debug('Event client not running, not stopping');
|
|
@@ -1935,11 +2001,23 @@ class Phone extends EventEmitter {
|
|
|
1935
2001
|
} catch {
|
|
1936
2002
|
this.log.warning('Unable to disconnect event client');
|
|
1937
2003
|
}
|
|
1938
|
-
|
|
1939
|
-
this._isStarted = false;
|
|
1940
2004
|
this.log.info('Phone client successfully stopped');
|
|
1941
2005
|
}
|
|
1942
2006
|
|
|
2007
|
+
/**
|
|
2008
|
+
* Re-send REGISTER without tearing down the websocket connection or any active calls.
|
|
2009
|
+
* Use this to recover from a REGISTER refresh that failed for a reason other than bad
|
|
2010
|
+
* credentials (e.g. a timeout or a 5xx from the registrar) while the transport itself stayed
|
|
2011
|
+
* up - JsSIP does not retry those on its own. Safe to call during an active call.
|
|
2012
|
+
* @throws NotReady - If phone is not started
|
|
2013
|
+
*/
|
|
2014
|
+
register() {
|
|
2015
|
+
if (!this._isStarted) {
|
|
2016
|
+
throw new NotReady('Phone is not started');
|
|
2017
|
+
}
|
|
2018
|
+
this._UA.register();
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1943
2021
|
/**
|
|
1944
2022
|
* Start a new call
|
|
1945
2023
|
* If you have other active calls they will be set on hold
|
|
@@ -2041,6 +2119,27 @@ class Phone extends EventEmitter {
|
|
|
2041
2119
|
return this._options;
|
|
2042
2120
|
}
|
|
2043
2121
|
|
|
2122
|
+
/**
|
|
2123
|
+
* Replace the call token used by subsequent calls and answers.
|
|
2124
|
+
* Call tokens expire (30 min); renew via the API and pass the new token here.
|
|
2125
|
+
*
|
|
2126
|
+
* With useCallDataInfoEvent, the callDataInfo subscription keeps the token from start();
|
|
2127
|
+
* call start() again to move it to the new token.
|
|
2128
|
+
*
|
|
2129
|
+
* @param {string} token New call token
|
|
2130
|
+
* @throws NotReady - If phone is not started
|
|
2131
|
+
* @throws InvalidOptions - If token is empty
|
|
2132
|
+
*/
|
|
2133
|
+
setCallToken(token) {
|
|
2134
|
+
if (!this._isStarted) {
|
|
2135
|
+
throw new NotReady('Phone is not started');
|
|
2136
|
+
}
|
|
2137
|
+
if (!token) {
|
|
2138
|
+
throw new InvalidOptions('callToken', 'Missing call token');
|
|
2139
|
+
}
|
|
2140
|
+
this._options.callToken = token;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2044
2143
|
/**
|
|
2045
2144
|
* Return list of current sessions
|
|
2046
2145
|
* @returns [Session]
|
|
@@ -2106,6 +2205,7 @@ class Phone extends EventEmitter {
|
|
|
2106
2205
|
*/
|
|
2107
2206
|
playTone(duration, freq1, freq2 = 0, callback = null) {
|
|
2108
2207
|
this.log.debug(`Playing tone ${freq1} / ${freq2} for ${duration} ms`);
|
|
2208
|
+
this._resumeToneContext();
|
|
2109
2209
|
|
|
2110
2210
|
this._oscillator1.frequency.value = freq1;
|
|
2111
2211
|
this._oscillator2.frequency.value = freq2;
|
|
@@ -2172,8 +2272,35 @@ class Phone extends EventEmitter {
|
|
|
2172
2272
|
}
|
|
2173
2273
|
}
|
|
2174
2274
|
|
|
2175
|
-
|
|
2275
|
+
/**
|
|
2276
|
+
* Resume the Web Audio context behind signal tones (ringing, ringback, busy, DTMF feedback).
|
|
2277
|
+
* Browsers create an AudioContext "suspended" when it is not created during a user gesture
|
|
2278
|
+
* (e.g. a phone started on page load); call this from a user-gesture handler (click, keydown)
|
|
2279
|
+
* so tones are audible. Safe to call before {@link start} and any number of times.
|
|
2280
|
+
* @return {Promise<boolean>} false when the browser has no Web Audio API or resuming failed
|
|
2281
|
+
*/
|
|
2282
|
+
async resumeAudio() {
|
|
2283
|
+
try {
|
|
2284
|
+
this._initToneGenerator();
|
|
2285
|
+
} catch {
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
try {
|
|
2289
|
+
await this._toneContext.resume();
|
|
2290
|
+
return this._toneContext.state === 'running';
|
|
2291
|
+
} catch (err) {
|
|
2292
|
+
this.log.warning('Unable to resume audio context: ' + err);
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Initialize tone generator. One AudioContext per Phone, reused across start()/stop() -
|
|
2298
|
+
// browsers cap the number of live contexts and a new one per start() would leak.
|
|
2176
2299
|
_initToneGenerator() {
|
|
2300
|
+
if (this._toneContext && this._toneContext.state !== 'closed') {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2177
2304
|
// Create audio context
|
|
2178
2305
|
const AudioContext = window.AudioContext || window.webkitAudioContext || false;
|
|
2179
2306
|
if (!AudioContext) {
|
|
@@ -2181,6 +2308,7 @@ class Phone extends EventEmitter {
|
|
|
2181
2308
|
}
|
|
2182
2309
|
|
|
2183
2310
|
const ctx = new AudioContext();
|
|
2311
|
+
this._toneContext = ctx;
|
|
2184
2312
|
this._toneStreamNode = ctx.createMediaStreamDestination();
|
|
2185
2313
|
this._toneAudioElement.srcObject = this._toneStreamNode.stream;
|
|
2186
2314
|
|
|
@@ -2200,6 +2328,14 @@ class Phone extends EventEmitter {
|
|
|
2200
2328
|
this._oscillator2.start();
|
|
2201
2329
|
}
|
|
2202
2330
|
|
|
2331
|
+
// Best effort: a context created outside a user gesture is suspended until resumed.
|
|
2332
|
+
_resumeToneContext() {
|
|
2333
|
+
const ctx = this._toneContext;
|
|
2334
|
+
if (ctx && ctx.state === 'suspended' && typeof ctx.resume === 'function') {
|
|
2335
|
+
ctx.resume().catch((err) => this.log.debug('Audio context resume refused: ' + err));
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2203
2339
|
/**
|
|
2204
2340
|
* Hold all active calls
|
|
2205
2341
|
* @throws NotAllowed If there are connection calls
|
|
@@ -2312,17 +2448,23 @@ class Phone extends EventEmitter {
|
|
|
2312
2448
|
* @property {string} REGISTRATION_FAILED Registration failed
|
|
2313
2449
|
*/
|
|
2314
2450
|
// Set connection state and emit event
|
|
2315
|
-
_setConnectionState(state) {
|
|
2451
|
+
_setConnectionState(state, extra = {}) {
|
|
2316
2452
|
/**
|
|
2317
2453
|
* @event connectionStateChange
|
|
2318
2454
|
* @type {object}
|
|
2319
2455
|
* @param {CONNECTION_STATE} state - The current connection state
|
|
2456
|
+
* @param {string} [cause] - For REGISTRATION_FAILED only: JsSIP's registrationFailed
|
|
2457
|
+
* cause, e.g. 'Connection Error', 'Request Timeout' (network/transport issues, JsSIP
|
|
2458
|
+
* will keep retrying on its own), 'Authentication Error' (401), or 'Rejected' (403 -
|
|
2459
|
+
* account disabled or credentials rejected by the registrar)
|
|
2460
|
+
* @param {number} [statusCode] - For REGISTRATION_FAILED only: the SIP status code of
|
|
2461
|
+
* the failed REGISTER response, when JsSIP received one (e.g. 401, 403)
|
|
2320
2462
|
*/
|
|
2321
2463
|
|
|
2322
2464
|
this.log.info('Connection state: ' + state);
|
|
2323
2465
|
this._connectionState = state;
|
|
2324
2466
|
try {
|
|
2325
|
-
this.emit('connectionStateChange', { state: state });
|
|
2467
|
+
this.emit('connectionStateChange', { state: state, ...extra });
|
|
2326
2468
|
} catch (e) {
|
|
2327
2469
|
this.log.error('Exception in connectionStateChange event: ' + e);
|
|
2328
2470
|
}
|
|
@@ -2370,18 +2512,15 @@ class Phone extends EventEmitter {
|
|
|
2370
2512
|
* @param {string} effectiveCidNameStr - Phone number name used as a-number for call
|
|
2371
2513
|
* @param {boolean} callPrivacy - Call privacy used for call (number hidden)
|
|
2372
2514
|
* @param {number} hangupCauseQ850 - Q.850 hangup cause code
|
|
2373
|
-
* @param {Object[]} recordings - List of recordings done for this call
|
|
2374
|
-
* @param {number} recordings[].startTime - Time recording was started
|
|
2375
|
-
* @param {number} recordings[].length - Recording length in milliseconds
|
|
2376
|
-
* @param {number} recordings[].size - Recording size (bytes)
|
|
2377
|
-
* @param {number} recordings[].urlPlayer - URL for recording with player
|
|
2378
|
-
* @param {number} recordings[].url - URL for recording without player
|
|
2379
|
-
* @param {number} recordings[].retentionDate - Date recording will be automatically deleted
|
|
2380
2515
|
*/
|
|
2381
2516
|
|
|
2382
2517
|
let e;
|
|
2383
2518
|
e = camelcaseKeys(eventData);
|
|
2384
|
-
|
|
2519
|
+
// The call data event no longer carries recordings. Uploads are
|
|
2520
|
+
// asynchronous, so the array was almost always empty by the time the CDR
|
|
2521
|
+
// was processed, and the event cannot be re-fired once a recording lands
|
|
2522
|
+
// because call_token is never persisted on the CDR. A dedicated
|
|
2523
|
+
// "recording ready" event will replace it.
|
|
2385
2524
|
|
|
2386
2525
|
// Event all duration fields are in seconds, convert to millisecs
|
|
2387
2526
|
e.duration = e.duration * 1000;
|
|
@@ -2474,7 +2613,7 @@ class Session extends EventEmitter {
|
|
|
2474
2613
|
this.destinationNumber = number;
|
|
2475
2614
|
|
|
2476
2615
|
let options = {
|
|
2477
|
-
mediaConstraints: { audio:
|
|
2616
|
+
mediaConstraints: { audio: this._phone.audioConstraints, video: false },
|
|
2478
2617
|
pcConfig: {
|
|
2479
2618
|
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
2480
2619
|
},
|
|
@@ -2828,7 +2967,7 @@ class Session extends EventEmitter {
|
|
|
2828
2967
|
*/
|
|
2829
2968
|
answer(metaInfo = null) {
|
|
2830
2969
|
let options = {
|
|
2831
|
-
mediaConstraints: { audio:
|
|
2970
|
+
mediaConstraints: { audio: this._phone.audioConstraints, video: false },
|
|
2832
2971
|
pcConfig: {
|
|
2833
2972
|
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
|
|
2834
2973
|
},
|