@webex/plugin-meetings 1.147.1 → 1.148.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.
package/src/constants.js CHANGED
@@ -1220,6 +1220,7 @@ export const METRICS_OPERATIONAL_MEASURES = {
1220
1220
  SET_MEETING_QUALITY_FAILURE: 'js_sdk_set_meeting_quality_failures',
1221
1221
  STOP_FLOOR_REQUEST_FAILURE: 'js_sdk_stop_floor_request_failures',
1222
1222
  ADD_DIAL_IN_FAILURE: 'js_sdk_add_dial_in_failure',
1223
+ ADD_DIAL_OUT_FAILURE: 'js_sdk_add_dial_out_failure',
1223
1224
  UPDATE_MEDIA_FAILURE: 'js_sdk_update_media_failures',
1224
1225
  UNMUTE_AUDIO_FAILURE: 'js_sdk_unmute_audio_failures',
1225
1226
  UNMUTE_VIDEO_FAILURE: 'js_sdk_unmute_video_failures',
@@ -1232,3 +1233,12 @@ export const METRICS_OPERATIONAL_MEASURES = {
1232
1233
  UPLOAD_LOGS_FAILURE: 'js_sdk_upload_logs_failure',
1233
1234
  RECEIVE_TRANSCRIPTION_FAILURE: 'js_sdk_receive_transcription_failure'
1234
1235
  };
1236
+
1237
+ export const PSTN_STATUS = {
1238
+ JOINED: 'JOINED', // we have provisioned a pstn device, which can be used to connect
1239
+ CONNECTED: 'CONNECTED', // user is connected to audio with pstn device
1240
+ LEFT: 'LEFT', // user has disconnected from the pstn device
1241
+ TRANSFERRING: 'TRANSFERRING', // usually happens in dial-out after the CONNECTED state
1242
+ SUCCESS: 'SUCCESS', // happens after the transfer (TRANSFERRING) is successful
1243
+ UNKNOWN: '' // placeholder if we haven't been told what the status is
1244
+ };
@@ -57,6 +57,7 @@ import {
57
57
  ONLINE,
58
58
  OFFLINE,
59
59
  PC_BAIL_TIMEOUT,
60
+ PSTN_STATUS,
60
61
  QUALITY_LEVELS,
61
62
  RECORDING_STATE,
62
63
  ROAP_SEQ_PRE,
@@ -710,13 +711,14 @@ export default class Meeting extends StatelessWebexPlugin {
710
711
  */
711
712
  this.floorGrantPending = false;
712
713
  /**
713
- * The latest status of the dial in device (can be "JOINED", "CONNECTED", "LEFT" or "")
714
+ * The latest status of the dial in device (can be "JOINED", "CONNECTED", "LEFT",
715
+ * "TRANSFERRING", "SUCCESS" or "")
714
716
  * @instance
715
717
  * @type {String}
716
718
  * @private
717
719
  * @memberof Meeting
718
720
  */
719
- this.dialInDeviceStatus = '';
721
+ this.dialInDeviceStatus = PSTN_STATUS.UNKNOWN;
720
722
  /**
721
723
  * the url for provisioned device used to dial in
722
724
  * @instance
@@ -725,6 +727,23 @@ export default class Meeting extends StatelessWebexPlugin {
725
727
  * @memberof Meeting
726
728
  */
727
729
  this.dialInUrl = '';
730
+ /**
731
+ * The latest status of the dial out device (can be "JOINED", "CONNECTED", "LEFT",
732
+ * "TRANSFERRING", "SUCCESS" or "")
733
+ * @instance
734
+ * @type {String}
735
+ * @private
736
+ * @memberof Meeting
737
+ */
738
+ this.dialOutDeviceStatus = PSTN_STATUS.UNKNOWN;
739
+ /**
740
+ * the url for provisioned device used to dial out
741
+ * @instance
742
+ * @type {String}
743
+ * @private
744
+ * @memberof Meeting
745
+ */
746
+ this.dialOutUrl = '';
728
747
  /**
729
748
  * @instance
730
749
  * @type {MediaMetrics}
@@ -1144,9 +1163,28 @@ export default class Meeting extends StatelessWebexPlugin {
1144
1163
  pstnUpdate(payload) {
1145
1164
  if (this.locusInfo.self) {
1146
1165
  const dialInPstnDevice = payload.newSelf?.pstnDevices.find((device) => device.url === this.dialInUrl);
1166
+ const dialOutPstnDevice = payload.newSelf?.pstnDevices.find((device) => device.url === this.dialOutUrl);
1167
+ let changed = false;
1147
1168
 
1148
1169
  if (dialInPstnDevice) {
1149
- this.dialInDeviceStatus = dialInPstnDevice.dialingStatus ?? dialInPstnDevice.state;
1170
+ const newStatus = dialInPstnDevice.dialingStatus ?? dialInPstnDevice.state;
1171
+
1172
+ if (newStatus !== this.dialInDeviceStatus) {
1173
+ this.dialInDeviceStatus = newStatus;
1174
+ changed = true;
1175
+ }
1176
+ }
1177
+
1178
+ if (dialOutPstnDevice) {
1179
+ const newStatus = dialOutPstnDevice.dialingStatus ?? dialOutPstnDevice.state;
1180
+
1181
+ if (newStatus !== this.dialOutDeviceStatus) {
1182
+ this.dialOutDeviceStatus = newStatus;
1183
+ changed = true;
1184
+ }
1185
+ }
1186
+
1187
+ if (changed) {
1150
1188
  Trigger.trigger(
1151
1189
  this,
1152
1190
  {
@@ -1157,7 +1195,11 @@ export default class Meeting extends StatelessWebexPlugin {
1157
1195
  {
1158
1196
  dialIn: {
1159
1197
  status: this.dialInDeviceStatus,
1160
- attendeeId: dialInPstnDevice.attendeeId
1198
+ attendeeId: dialInPstnDevice?.attendeeId
1199
+ },
1200
+ dialOut: {
1201
+ status: this.dialOutDeviceStatus,
1202
+ attendeeId: dialOutPstnDevice?.attendeeId
1161
1203
  }
1162
1204
  }
1163
1205
  );
@@ -3313,7 +3355,7 @@ export default class Meeting extends StatelessWebexPlugin {
3313
3355
  /**
3314
3356
  * Use phone for meeting audio
3315
3357
  * @param {String} phoneNumber If provided, it will dial-out using this number. If not provided, dial-in will be used
3316
- * @returns {Promise} Resolves once the dial-in or dial-out request has completed
3358
+ * @returns {Promise} Resolves once the dial-in or dial-out request has completed, or rejects if it failed
3317
3359
  * @public
3318
3360
  * @memberof Meeting
3319
3361
  */
@@ -3322,17 +3364,28 @@ export default class Meeting extends StatelessWebexPlugin {
3322
3364
  return this.dialInPstn();
3323
3365
  }
3324
3366
 
3325
- return Promise.reject(new Error('Dial-out is not supported yet'));
3367
+ return this.dialOutPstn(phoneNumber);
3368
+ }
3369
+
3370
+ /**
3371
+ * Determines if the given pstnStatus is in a state which implies the phone is provisioned
3372
+ * @param {String} pstnStatus
3373
+ * @returns {Boolean}
3374
+ * @private
3375
+ * @memberof Meeting
3376
+ */
3377
+ isPhoneProvisioned(pstnStatus) {
3378
+ return [PSTN_STATUS.JOINED, PSTN_STATUS.CONNECTED, PSTN_STATUS.SUCCESS].includes(pstnStatus);
3326
3379
  }
3327
3380
 
3328
3381
  /**
3329
3382
  * Enable dial-in for audio
3330
- * @returns {Promise} Resolves once the dial-in request has completed
3383
+ * @returns {Promise} Resolves once the dial-in request has completed, or rejects if it failed
3331
3384
  * @private
3332
3385
  * @memberof Meeting
3333
3386
  */
3334
3387
  dialInPstn() {
3335
- if (this.dialInDeviceStatus === 'CONNECTED' || this.dialInDeviceStatus === 'JOINED') return Promise.resolve(); // prevent multiple dial in devices from being provisioned
3388
+ if (this.isPhoneProvisioned(this.dialInDeviceStatus)) return Promise.resolve(); // prevent multiple dial in devices from being provisioned
3336
3389
 
3337
3390
  const {correlationId, locusUrl} = this;
3338
3391
 
@@ -3357,6 +3410,47 @@ export default class Meeting extends StatelessWebexPlugin {
3357
3410
  stack: error.stack
3358
3411
  }
3359
3412
  );
3413
+
3414
+ return Promise.reject(error);
3415
+ });
3416
+ }
3417
+
3418
+ /**
3419
+ * Enable dial-out for audio
3420
+ * @param {String} phoneNumber Phone number to dial out to
3421
+ * @returns {Promise} Resolves once the dial-out request has completed (it doesn't wait for the user to answer the phone), or rejects if it failed
3422
+ * @private
3423
+ * @memberof Meeting
3424
+ */
3425
+ dialOutPstn(phoneNumber) {
3426
+ if (this.isPhoneProvisioned(this.dialOutDeviceStatus)) return Promise.resolve(); // prevent multiple dial out devices from being provisioned
3427
+
3428
+ const {correlationId, locusUrl} = this;
3429
+
3430
+ if (!this.dialOutUrl) this.dialOutUrl = `dialout:///${uuid.v4()}`;
3431
+
3432
+ return this.meetingRequest.dialOut({
3433
+ correlationId,
3434
+ dialOutUrl: this.dialOutUrl,
3435
+ phoneNumber,
3436
+ locusUrl,
3437
+ clientUrl: this.deviceUrl
3438
+ }).then((res) => {
3439
+ this.locusInfo.onFullLocus(res.body.locus);
3440
+ }).catch((error) => {
3441
+ Metrics.sendOperationalMetric(
3442
+ METRICS_OPERATIONAL_MEASURES.ADD_DIAL_OUT_FAILURE,
3443
+ {
3444
+ correlation_id: this.correlationId,
3445
+ dial_out_url: this.dialOutUrl,
3446
+ locus_id: locusUrl.split('/').pop(),
3447
+ client_url: this.deviceUrl,
3448
+ reason: error.error?.message,
3449
+ stack: error.stack
3450
+ }
3451
+ );
3452
+
3453
+ return Promise.reject(error);
3360
3454
  });
3361
3455
  }
3362
3456
 
@@ -3368,13 +3462,14 @@ export default class Meeting extends StatelessWebexPlugin {
3368
3462
  * @returns {Promise}
3369
3463
  */
3370
3464
  disconnectPhoneAudio() {
3371
- if (this.dialInDeviceStatus === 'CONNECTED') {
3372
- return MeetingUtil.leavePstn(this, this.dialInUrl);
3373
- }
3374
-
3375
- // TODO: handle dial out
3376
-
3377
- return Promise.resolve();
3465
+ return Promise.all([
3466
+ this.isPhoneProvisioned(this.dialInDeviceStatus) ?
3467
+ MeetingUtil.disconnectPhoneAudio(this, this.dialInUrl) :
3468
+ Promise.resolve(),
3469
+ this.isPhoneProvisioned(this.dialOutDeviceStatus) ?
3470
+ MeetingUtil.disconnectPhoneAudio(this, this.dialOutUrl) :
3471
+ Promise.resolve()
3472
+ ]);
3378
3473
  }
3379
3474
 
3380
3475
  /**
@@ -20,6 +20,7 @@ import {
20
20
  MEDIA,
21
21
  PARTICIPANT,
22
22
  PROVISIONAL_TYPE_DIAL_IN,
23
+ PROVISIONAL_TYPE_DIAL_OUT,
23
24
  SEND_DTMF_ENDPOINT,
24
25
  _SLIDES_
25
26
  } from '../constants';
@@ -182,6 +183,48 @@ export default class MeetingRequest extends StatelessWebexPlugin {
182
183
  });
183
184
  }
184
185
 
186
+ /**
187
+ * Make a network request to add a dial out device
188
+ * @param {Object} options
189
+ * @param {String} options.correlationId
190
+ * @param {String} options.localUrl url for the meeting
191
+ * @param {String} options.dialOutUrl identifier for the to-be provisioned device
192
+ * @param {String} options.phoneNumber phone number to dial out to
193
+ * @param {String} options.clientUrl identifier for the web device
194
+ * @returns {Promise}
195
+ * @private
196
+ */
197
+ dialOut({
198
+ locusUrl, dialOutUrl, phoneNumber, clientUrl, correlationId
199
+ }) {
200
+ LoggerProxy.logger.info(
201
+ 'Meeting:request#dialOut --> Provisioning a dial out device',
202
+ correlationId
203
+ );
204
+ const uri = `${locusUrl}/${PARTICIPANT}`;
205
+
206
+ const body = {
207
+ device: {
208
+ deviceType: deviceType.PROVISIONAL,
209
+ provisionalType: PROVISIONAL_TYPE_DIAL_OUT,
210
+ url: dialOutUrl,
211
+ dialoutAddress: phoneNumber,
212
+ clientUrl
213
+ },
214
+ correlationId
215
+ };
216
+
217
+ return this.request({
218
+ method: HTTP_VERBS.POST,
219
+ uri,
220
+ body
221
+ }).catch((err) => {
222
+ LoggerProxy.logger.error(`Meeting:request#dialOut --> Error provisioning a dial out device, error ${err}`);
223
+
224
+ throw err;
225
+ });
226
+ }
227
+
185
228
  /**
186
229
  * Syns the missed delta event
187
230
  * @param {Object} options
@@ -241,21 +284,20 @@ export default class MeetingRequest extends StatelessWebexPlugin {
241
284
  }
242
285
 
243
286
  /**
244
- * Make a network request to make a provisioned pstn device leave the meeting
287
+ * Make a network request to make a provisioned phone leave the meeting
245
288
  * @param {Object} options
246
289
  * @param {String} options.locusUrl
247
- * @param {String} options.selfId
248
- * @param {String} options.deviceUrl
249
- * @param {String} options.resourceId,
290
+ * @param {String} options.phoneUrl
250
291
  * @param {String} options.correlationId
292
+ * @param {String} options.selfId
251
293
  * @returns {Promise}
252
294
  * @private
253
295
  */
254
- leavePstn({
255
- locusUrl, dialInUrl, correlationId, selfId
296
+ disconnectPhoneAudio({
297
+ locusUrl, phoneUrl, correlationId, selfId
256
298
  }) {
257
299
  LoggerProxy.logger.info(
258
- `Meeting:request#leavePstn --> request device ${dialInUrl} to leaving`,
300
+ `Meeting:request#disconnectPhoneAudio --> request phone ${phoneUrl} to leave`,
259
301
  correlationId
260
302
  );
261
303
  const uri = `${locusUrl}/${PARTICIPANT}/${selfId}/${LEAVE}`;
@@ -263,7 +305,7 @@ export default class MeetingRequest extends StatelessWebexPlugin {
263
305
  const body = {
264
306
  device: {
265
307
  deviceType: deviceType.PROVISIONAL,
266
- url: dialInUrl
308
+ url: phoneUrl
267
309
  },
268
310
  correlationId
269
311
  };
@@ -273,7 +315,9 @@ export default class MeetingRequest extends StatelessWebexPlugin {
273
315
  uri,
274
316
  body
275
317
  }).catch((err) => {
276
- LoggerProxy.logger.error(`Meeting:request#leavePstn --> Error when requesting pstn device ${dialInUrl} to leave, error ${err}`);
318
+ LoggerProxy.logger.error(
319
+ `Meeting:request#disconnectPhoneAudio --> Error when requesting phone ${phoneUrl} to leave, error ${err}`
320
+ );
277
321
 
278
322
  throw err;
279
323
  });
@@ -134,7 +134,7 @@ MeetingUtil.cleanUp = (meeting) => {
134
134
  .then(() => meeting.roap.stop(meeting.correlationId, meeting.roapSeq));
135
135
  };
136
136
 
137
- MeetingUtil.leavePstn = (meeting, dialInUrl) => {
137
+ MeetingUtil.disconnectPhoneAudio = (meeting, phoneUrl) => {
138
138
  if (meeting.meetingState === FULL_STATE.INACTIVE) {
139
139
  return Promise.reject(new MeetingNotActiveError());
140
140
  }
@@ -143,11 +143,11 @@ MeetingUtil.leavePstn = (meeting, dialInUrl) => {
143
143
  locusUrl: meeting.locusUrl,
144
144
  selfId: meeting.selfId,
145
145
  correlationId: meeting.correlationId,
146
- dialInUrl
146
+ phoneUrl
147
147
  };
148
148
 
149
149
  return meeting.meetingRequest
150
- .leavePstn(options)
150
+ .disconnectPhoneAudio(options)
151
151
  .then((response) => {
152
152
  if (response?.body?.locus) {
153
153
  meeting.locusInfo.onFullLocus(response.body.locus);
@@ -155,7 +155,7 @@ MeetingUtil.leavePstn = (meeting, dialInUrl) => {
155
155
  })
156
156
  .catch((err) => {
157
157
  LoggerProxy.logger.error(
158
- `Meeting:util#leavePstn --> An error occured while trying to leave pstn with an id of ${
158
+ `Meeting:util#disconnectPhoneAudio --> An error occured while disconnecting phone audio in meeting ${
159
159
  meeting.id
160
160
  }, error: ${err}`
161
161
  );
@@ -1902,7 +1902,9 @@ describe('plugin-meetings', () => {
1902
1902
 
1903
1903
  describe('#usePhoneAudio', () => {
1904
1904
  beforeEach(() => {
1905
- meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.resolve());
1905
+ meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.resolve({body: {locus: 'testData'}}));
1906
+ meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.resolve({body: {locus: 'testData'}}));
1907
+ meeting.locusInfo.onFullLocus = sinon.stub().returns(Promise.resolve());
1906
1908
  });
1907
1909
 
1908
1910
  it('with no parameters triggers dial-in, delegating request to meetingRequest correctly', async () => {
@@ -1915,8 +1917,11 @@ describe('plugin-meetings', () => {
1915
1917
  locusUrl: meeting.locusUrl,
1916
1918
  clientUrl: meeting.deviceUrl
1917
1919
  });
1920
+ assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
1921
+ assert.notCalled(meeting.meetingRequest.dialOut);
1918
1922
 
1919
1923
  meeting.meetingRequest.dialIn.resetHistory();
1924
+ meeting.locusInfo.onFullLocus.resetHistory();
1920
1925
 
1921
1926
  // try again. the dial in urls should match
1922
1927
  await meeting.usePhoneAudio();
@@ -1927,6 +1932,65 @@ describe('plugin-meetings', () => {
1927
1932
  locusUrl: meeting.locusUrl,
1928
1933
  clientUrl: meeting.deviceUrl
1929
1934
  });
1935
+ assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
1936
+ assert.notCalled(meeting.meetingRequest.dialOut);
1937
+ });
1938
+
1939
+ it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
1940
+ const phoneNumber = '+442088241000';
1941
+
1942
+ await meeting.usePhoneAudio(phoneNumber);
1943
+ const DIAL_OUT_URL = meeting.dialOutUrl;
1944
+
1945
+ assert.calledWith(meeting.meetingRequest.dialOut, {
1946
+ correlationId: meeting.correlationId,
1947
+ dialOutUrl: DIAL_OUT_URL,
1948
+ locusUrl: meeting.locusUrl,
1949
+ clientUrl: meeting.deviceUrl,
1950
+ phoneNumber
1951
+ });
1952
+ assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
1953
+ assert.notCalled(meeting.meetingRequest.dialIn);
1954
+
1955
+ meeting.meetingRequest.dialOut.resetHistory();
1956
+ meeting.locusInfo.onFullLocus.resetHistory();
1957
+
1958
+ // try again. the dial out urls should match
1959
+ await meeting.usePhoneAudio(phoneNumber);
1960
+
1961
+ assert.calledWith(meeting.meetingRequest.dialOut, {
1962
+ correlationId: meeting.correlationId,
1963
+ dialOutUrl: DIAL_OUT_URL,
1964
+ locusUrl: meeting.locusUrl,
1965
+ clientUrl: meeting.deviceUrl,
1966
+ phoneNumber
1967
+ });
1968
+ assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
1969
+ assert.notCalled(meeting.meetingRequest.dialIn);
1970
+ });
1971
+
1972
+ it('rejects if the request failed (dial in)', () => {
1973
+ const error = 'something bad happened';
1974
+
1975
+ meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
1976
+
1977
+ return meeting.usePhoneAudio().then(() => Promise.reject(new Error('Promise resolved when it should have rejected'))).catch((e) => {
1978
+ assert.equal(e, error);
1979
+
1980
+ return Promise.resolve();
1981
+ });
1982
+ });
1983
+
1984
+ it('rejects if the request failed (dial out)', async () => {
1985
+ const error = 'something bad happened';
1986
+
1987
+ meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
1988
+
1989
+ return meeting.usePhoneAudio('+441234567890').then(() => Promise.reject(new Error('Promise resolved when it should have rejected'))).catch((e) => {
1990
+ assert.equal(e, error);
1991
+
1992
+ return Promise.resolve();
1993
+ });
1930
1994
  });
1931
1995
  });
1932
1996
 
@@ -152,23 +152,48 @@ describe('plugin-meetings', () => {
152
152
  assert.equal(requestParams.body.device.clientUrl, 'clientUrl');
153
153
  });
154
154
 
155
- it('sends leave pstn request', async () => {
155
+ it('sends dial out pstn request', async () => {
156
+ const locusUrl = 'locusUrl';
157
+ const clientUrl = 'clientUrl';
158
+ const correlationId = 'random-uuid';
159
+ const dialOutUrl = 'url';
160
+ const phoneNumber = '+442088241000';
161
+
162
+ await meetingsRequest.dialOut({
163
+ locusUrl,
164
+ clientUrl,
165
+ correlationId,
166
+ dialOutUrl,
167
+ phoneNumber
168
+ });
169
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
170
+
171
+ assert.equal(requestParams.method, 'POST');
172
+ assert.equal(requestParams.uri, `${locusUrl}/participant`);
173
+ assert.equal(requestParams.body.device.url, dialOutUrl);
174
+ assert.equal(requestParams.body.device.deviceType, 'PROVISIONAL');
175
+ assert.equal(requestParams.body.device.provisionalType, 'DIAL_OUT');
176
+ assert.equal(requestParams.body.device.clientUrl, 'clientUrl');
177
+ assert.equal(requestParams.body.device.dialoutAddress, phoneNumber);
178
+ });
179
+
180
+ it('sends disconnect phone audio request', async () => {
156
181
  const locusUrl = 'locusUrl';
157
182
  const selfId = 'selfId';
158
183
  const correlationId = 'random-uuid';
159
- const dialInUrl = 'url';
184
+ const phoneUrl = 'url';
160
185
 
161
- await meetingsRequest.leavePstn({
186
+ await meetingsRequest.disconnectPhoneAudio({
162
187
  locusUrl,
163
188
  selfId,
164
189
  correlationId,
165
- dialInUrl
190
+ phoneUrl
166
191
  });
167
192
  const requestParams = meetingsRequest.request.getCall(0).args[0];
168
193
 
169
194
  assert.equal(requestParams.method, 'PUT');
170
195
  assert.equal(requestParams.uri, `${locusUrl}/participant/${selfId}/leave`);
171
- assert.equal(requestParams.body.device.url, dialInUrl);
196
+ assert.equal(requestParams.body.device.url, phoneUrl);
172
197
  assert.equal(requestParams.body.device.deviceType, 'PROVISIONAL');
173
198
  });
174
199
  });