@webex/plugin-meetings 2.12.0 → 2.14.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.
@@ -0,0 +1,292 @@
1
+ /* eslint-disable camelcase */
2
+ import {assert} from '@webex/test-helper-chai';
3
+ import sinon from 'sinon';
4
+ import MockWebex from '@webex/test-helper-mock-webex';
5
+
6
+ import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
7
+ import {BNR_STATUS} from '@webex/plugin-meetings/src/constants';
8
+ import Meeting from '@webex/plugin-meetings/src/meeting';
9
+ import Meetings from '@webex/plugin-meetings';
10
+ import Metrics from '@webex/plugin-meetings/src/metrics';
11
+ import MediaUtil from '@webex/plugin-meetings/src/media/util';
12
+ import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
13
+ import createEffectsState from '@webex/plugin-meetings/src/meeting/effectsState';
14
+ import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
15
+ import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
16
+
17
+ describe('plugin-meetings', () => {
18
+ const logger = {
19
+ info: () => {},
20
+ log: () => {},
21
+ error: () => {},
22
+ warn: () => {},
23
+ trace: () => {},
24
+ debug: () => {}
25
+ };
26
+
27
+ beforeEach(() => {
28
+ sinon.stub(Metrics, 'sendBehavioralMetric');
29
+ });
30
+ afterEach(() => {
31
+ sinon.restore();
32
+ });
33
+
34
+ Object.defineProperty(global.window.navigator, 'mediaDevices', {
35
+ writable: true,
36
+ value: {
37
+ getSupportedConstraints: sinon.stub().returns({
38
+ sampleRate: true
39
+ })
40
+ },
41
+ });
42
+ LoggerConfig.set({verboseEvents: true, enable: false});
43
+ LoggerProxy.set(logger);
44
+
45
+ let webex;
46
+ let meeting;
47
+ let uuid1;
48
+
49
+ const fakeMediaTrack = () => ({
50
+ id: Date.now().toString(),
51
+ stop: () => {},
52
+ readyState: 'live',
53
+ enabled: true,
54
+ getSettings: () => ({
55
+ sampleRate: 48000
56
+ })
57
+ });
58
+
59
+ class FakeMediaStream {
60
+ constructor(tracks) {
61
+ this.active = false;
62
+ this.id = '5146425f-c240-48cc-b86b-27d422988fb7';
63
+ this.tracks = tracks;
64
+ }
65
+
66
+ addTrack = () => undefined;
67
+
68
+ getAudioTracks = () => this.tracks;
69
+ }
70
+
71
+ class FakeAudioContext {
72
+ constructor() {
73
+ this.state = 'running';
74
+ this.baseLatency = 0.005333333333333333;
75
+ this.currentTime = 2.7946666666666666;
76
+ this.sampleRate = 48000;
77
+ this.audioWorklet = {
78
+ addModule: async () => undefined,
79
+ };
80
+ }
81
+
82
+ onstatechange = null;
83
+
84
+ createMediaStreamSource() {
85
+ return {
86
+ connect: () => undefined,
87
+ mediaStream: {
88
+ getAudioTracks() {
89
+ // eslint-disable-next-line no-undef
90
+ return [new MediaStreamTrack()];
91
+ },
92
+ },
93
+ };
94
+ }
95
+
96
+ createMediaStreamDestination() {
97
+ return {
98
+ stream: {
99
+ getAudioTracks() {
100
+ // eslint-disable-next-line no-undef
101
+ return [new MediaStreamTrack()];
102
+ },
103
+ },
104
+ };
105
+ }
106
+ }
107
+
108
+ class FakeAudioWorkletNode {
109
+ constructor() {
110
+ this.port = {
111
+ postMessage: () => undefined,
112
+ };
113
+ }
114
+
115
+ connect() {
116
+ /* placeholder method */
117
+ }
118
+ }
119
+
120
+ class FakeMediaStreamTrack {
121
+ constructor() {
122
+ this.kind = 'audio';
123
+ this.enabled = true;
124
+ this.label = 'Default - MacBook Pro Microphone (Built-in)';
125
+ this.muted = false;
126
+ this.readyState = 'live';
127
+ this.contentHint = '';
128
+ }
129
+
130
+ getSettings() {
131
+ return {
132
+ sampleRate: 48000
133
+ };
134
+ }
135
+ }
136
+ Object.defineProperty(global, 'MediaStream', {
137
+ writable: true,
138
+ value: FakeMediaStream,
139
+ });
140
+
141
+ Object.defineProperty(global, 'AudioContext', {
142
+ writable: true,
143
+ value: FakeAudioContext,
144
+ });
145
+
146
+ Object.defineProperty(global, 'AudioWorkletNode', {
147
+ writable: true,
148
+ value: FakeAudioWorkletNode,
149
+ });
150
+
151
+ Object.defineProperty(global, 'MediaStreamTrack', {
152
+ writable: true,
153
+ value: FakeMediaStreamTrack,
154
+ });
155
+
156
+ let effects;
157
+
158
+ beforeEach(() => {
159
+ webex = new MockWebex({
160
+ children: {
161
+ meetings: Meetings
162
+ }
163
+ });
164
+ MediaUtil.createPeerConnection = sinon.stub().returns({});
165
+ meeting = new Meeting(
166
+ {
167
+ userId: uuid1
168
+ },
169
+ {
170
+ parent: webex
171
+ }
172
+ );
173
+
174
+ effects = createEffectsState('BNR');
175
+ meeting.canUpdateMedia = sinon.stub().returns(true);
176
+ MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
177
+ MeetingUtil.updateTransceiver = sinon.stub();
178
+
179
+ meeting.addMedia = sinon.stub().returns(Promise.resolve());
180
+ meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
181
+ sinon.replace(meeting, 'addMedia', () => {
182
+ sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
183
+ sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
184
+ receiveAudio: true
185
+ });
186
+ });
187
+ });
188
+
189
+ describe('bnr effect library', () => {
190
+ beforeEach(async () => {
191
+ await meeting.getMediaStreams();
192
+ await meeting.addMedia();
193
+ });
194
+ describe('#enableBNR', () => {
195
+ it('should have #enableBnr', () => {
196
+ assert.exists(effects.enableBNR);
197
+ });
198
+
199
+ it('does bnr effect enable on audio track', async () => {
200
+ assert.isTrue(await effects.handleClientRequest(true, meeting));
201
+ assert.equal(effects.state.bnr.enabled, BNR_STATUS.ENABLED);
202
+
203
+ assert(Metrics.sendBehavioralMetric.calledOnce);
204
+ assert.calledWith(
205
+ Metrics.sendBehavioralMetric,
206
+ BEHAVIORAL_METRICS.ENABLE_BNR_SUCCESS,
207
+ );
208
+ });
209
+
210
+ it('does resolve request if bnr is already enabled', async () => {
211
+ effects.state.bnr.enabled = BNR_STATUS.ENABLED;
212
+ assert.isTrue(await effects.handleClientRequest(true, meeting));
213
+ assert.equal(effects.state.bnr.enabled, BNR_STATUS.ENABLED);
214
+ });
215
+
216
+ it('if called twice, does bnr effect enable on audio track for the first request and resolves second', async () => {
217
+ Promise.all([effects.handleClientRequest(true, meeting), effects.handleClientRequest(true, meeting)])
218
+ .then((resolveFirst, resolveSecond) => {
219
+ assert.isTrue(resolveFirst);
220
+ assert.isTrue(resolveSecond);
221
+ assert.calledOnce(MediaUtil.createMediaStream);
222
+ });
223
+ });
224
+
225
+ it('should throw error for inappropriate sample rate and send error metrics', async () => {
226
+ const fakeMediaTrack1 = () => ({
227
+ id: Date.now().toString(),
228
+ stop: () => {},
229
+ readyState: 'live',
230
+ getSettings: () => ({
231
+ sampleRate: 49000
232
+ })
233
+ });
234
+
235
+ sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack1());
236
+
237
+ // eslint-disable-next-line no-undef
238
+ MediaUtil.createMediaStream = sinon.stub().returns(new MediaStream([fakeMediaTrack1()]));
239
+ try {
240
+ await effects.handleClientRequest(true, meeting);
241
+ }
242
+ catch (err) {
243
+ assert(Metrics.sendBehavioralMetric.calledOnce);
244
+ assert.calledWith(
245
+ Metrics.sendBehavioralMetric,
246
+ BEHAVIORAL_METRICS.ENABLE_BNR_FAILURE, {
247
+ reason: err.message,
248
+ stack: err.stack
249
+ }
250
+ );
251
+ assert.equal(err.message, 'Sample rate of 49000 is not supported.');
252
+ }
253
+ });
254
+ });
255
+
256
+ describe('#disableBNR', () => {
257
+ beforeEach(() => {
258
+ effects.state.bnr.enabled = BNR_STATUS.ENABLED;
259
+ });
260
+ it('should have #disableBnr', () => {
261
+ assert.exists(effects.disableBNR);
262
+ });
263
+
264
+ it('does bnr disable on audio track', async () => {
265
+ assert.isTrue(await effects.handleClientRequest(false, meeting));
266
+ assert.equal(effects.state.bnr.enabled, BNR_STATUS.NOT_ENABLED);
267
+
268
+ assert(Metrics.sendBehavioralMetric.calledOnce);
269
+ assert.calledWith(
270
+ Metrics.sendBehavioralMetric,
271
+ BEHAVIORAL_METRICS.DISABLE_BNR_SUCCESS,
272
+ );
273
+ });
274
+
275
+ it('reject request for disable bnr if not enabled', async () => {
276
+ try {
277
+ await effects.handleClientRequest(false, meeting);
278
+ }
279
+ catch (e) {
280
+ assert.equal(e.message, 'Can not disable as BNR is not enabled');
281
+ assert.equal(effects.state.bnr.enabled, BNR_STATUS.ENABLED);
282
+
283
+ assert(Metrics.sendBehavioralMetric.calledOnce);
284
+ assert.calledWith(
285
+ Metrics.sendBehavioralMetric,
286
+ BEHAVIORAL_METRICS.DISABLE_BNR_FAILURE,
287
+ );
288
+ }
289
+ });
290
+ });
291
+ });
292
+ });
@@ -456,114 +456,25 @@ describe('plugin-meetings', () => {
456
456
  });
457
457
  });
458
458
  describe('BNR', () => {
459
- class FakeMediaStream {
460
- constructor() {
461
- this.active = false;
462
- this.id = '5146425f-c240-48cc-b86b-27d422988fb7';
463
- }
464
-
465
- addTrack = () => undefined;
466
- }
467
-
468
- class FakeAudioContext {
469
- constructor() {
470
- this.state = 'running';
471
- this.baseLatency = 0.005333333333333333;
472
- this.currentTime = 2.7946666666666666;
473
- this.sampleRate = 48000;
474
- this.audioWorklet = {
475
- addModule: async () => undefined,
476
- };
477
- }
478
-
479
- onstatechange = null;
480
-
481
- createMediaStreamSource() {
482
- return {
483
- connect: () => undefined,
484
- mediaStream: {
485
- getAudioTracks() {
486
- // eslint-disable-next-line no-undef
487
- return [new MediaStreamTrack()];
488
- },
489
- },
490
- };
491
- }
492
-
493
- createMediaStreamDestination() {
494
- return {
495
- stream: {
496
- getAudioTracks() {
497
- // eslint-disable-next-line no-undef
498
- return [new MediaStreamTrack()];
499
- },
500
- },
501
- };
502
- }
503
- }
504
-
505
- class FakeAudioWorkletNode {
506
- constructor() {
507
- this.port = {
508
- postMessage: () => undefined,
509
- };
510
- }
511
-
512
- connect() {
513
- /* placeholder method */
514
- }
515
- }
516
-
517
- class FakeMediaStreamTrack {
518
- constructor() {
519
- this.kind = 'audio';
520
- this.enabled = true;
521
- this.label = 'Default - MacBook Pro Microphone (Built-in)';
522
- this.muted = false;
523
- this.readyState = 'live';
524
- this.contentHint = '';
525
- }
526
- }
527
- Object.defineProperty(global, 'MediaStream', {
528
- writable: true,
529
- value: FakeMediaStream,
530
- });
531
-
532
- Object.defineProperty(global, 'AudioContext', {
533
- writable: true,
534
- value: FakeAudioContext,
535
- });
536
-
537
- Object.defineProperty(global, 'AudioWorkletNode', {
538
- writable: true,
539
- value: FakeAudioWorkletNode,
540
- });
541
-
542
- Object.defineProperty(global, 'MediaStreamTrack', {
543
- writable: true,
544
- value: FakeMediaStreamTrack,
459
+ const fakeMediaTrack = () => ({
460
+ id: Date.now().toString(),
461
+ stop: () => {},
462
+ readyState: 'live',
463
+ enabled: true,
464
+ getSettings: () => ({
465
+ sampleRate: 48000
466
+ })
545
467
  });
546
468
 
547
- beforeEach(async () => {
548
- meeting.canUpdateMedia = sinon.stub().returns(true);
549
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
550
- MeetingUtil.updateTransceiver = sinon.stub();
551
- const fakeMediaTrack = () => ({
552
- stop: () => {},
553
- readyState: 'live',
554
- getSettings: () => ({
555
- sampleRate: 48000
556
- })
557
- });
558
-
469
+ beforeEach(() => {
559
470
  meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
560
471
  sinon.replace(meeting, 'addMedia', () => {
561
472
  sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
473
+ sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
474
+ receiveAudio: true
475
+ });
562
476
  });
563
- await meeting.getMediaStreams();
564
- await meeting.addMedia();
565
477
  });
566
-
567
478
  describe('#enableBNR', () => {
568
479
  it('should have #enableBnr', () => {
569
480
  assert.exists(meeting.enableBNR);
@@ -578,67 +489,68 @@ describe('plugin-meetings', () => {
578
489
  });
579
490
 
580
491
  describe('after audio attached to meeting', () => {
581
- it('should return true for appropriate sample rate', async () => {
582
- const response = await meeting.enableBNR();
492
+ let handleClientRequest;
583
493
 
584
- assert.equal(response, true);
494
+ beforeEach(async () => {
495
+ await meeting.getMediaStreams();
496
+ await meeting.addMedia();
585
497
  });
586
498
 
587
- it('should throw error for inappropriate sample rate and send error metrics', async () => {
588
- const fakeMediaTrack = () => ({
589
- stop: () => {},
590
- readyState: 'live',
591
- getSettings: () => ({
592
- sampleRate: 49000
593
- })
594
- });
499
+ it('should throw error if meeting audio is muted', async () => {
500
+ const handleClientRequest = (meeting, mute) => {
501
+ meeting.mediaProperties.audioTrack.enabled = !mute;
502
+
503
+ return Promise.resolve();
504
+ };
505
+ const isMuted = () => !meeting.mediaProperties.audioTrack.enabled;
595
506
 
596
- sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
507
+ meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
508
+ meeting.mediaId = 'mediaId';
509
+ meeting.audio = {handleClientRequest, isMuted};
510
+ await meeting.muteAudio();
597
511
  await meeting.enableBNR().catch((err) => {
598
- assert(Metrics.sendBehavioralMetric.calledOnce);
599
- assert.calledWith(
600
- Metrics.sendBehavioralMetric,
601
- BEHAVIORAL_METRICS.ENABLE_BNR_FAILURE, {
602
- reason: err.message,
603
- stack: err.stack
604
- }
605
- );
606
- assert.equal(err.message, 'Sample rate of 49000 is not supported.');
512
+ assert.equal(err.message, 'Cannot enable BNR while meeting is muted');
607
513
  });
608
514
  });
609
515
 
610
- it('should send metrics for enable bnr success', async () => {
516
+ it('should return true on enable bnr success', async () => {
517
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
518
+ meeting.effects = {handleClientRequest};
611
519
  const response = await meeting.enableBNR();
612
520
 
613
- assert(Metrics.sendBehavioralMetric.calledOnce);
614
- assert.calledWith(
615
- Metrics.sendBehavioralMetric,
616
- BEHAVIORAL_METRICS.ENABLE_BNR_SUCCESS,
617
- );
618
521
  assert.equal(response, true);
619
522
  });
620
523
  });
621
524
  });
622
525
 
623
526
  describe('#disableBNR', () => {
624
- it('should have #disableBnr', () => {
625
- assert.exists(meeting.disableBNR);
527
+ describe('before audio attached to meeting', () => {
528
+ it('should have #disableBnr', () => {
529
+ assert.exists(meeting.disableBNR);
530
+ });
531
+
532
+ it('should throw no audio error', async () => {
533
+ await meeting.disableBNR().catch((err) => {
534
+ assert.equal(err.toString(), 'Error: Meeting doesn\'t have an audioTrack attached');
535
+ });
536
+ });
626
537
  });
538
+ describe('after audio attached to meeting', () => {
539
+ beforeEach(async () => {
540
+ await meeting.getMediaStreams();
541
+ await meeting.addMedia();
542
+ });
627
543
 
628
- it('should return true if bnr is disabled on bnr enabled track', async () => {
629
- const response = await meeting.disableBNR();
544
+ let handleClientRequest;
545
+ let isBnrEnabled;
630
546
 
631
- assert.equal(response, true);
632
- });
547
+ it('should return true on disable bnr success', async () => {
548
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
549
+ isBnrEnabled = sinon.stub().returns(Promise.resolve(true));
550
+ meeting.effects = {handleClientRequest, isBnrEnabled};
551
+ const response = await meeting.disableBNR();
633
552
 
634
- it('should throw error if bnr is not enabled before disabling and send error metrics', async () => {
635
- await meeting.disableBNR().catch((err) => {
636
- assert(Metrics.sendBehavioralMetric.calledOnce);
637
- assert.calledWith(
638
- Metrics.sendBehavioralMetric,
639
- BEHAVIORAL_METRICS.DISABLE_BNR_FAILURE,
640
- );
641
- assert.equal(err.message, 'Can not disable as BNR is not enabled');
553
+ assert.equal(response, true);
642
554
  });
643
555
  });
644
556
  });
@@ -88,10 +88,14 @@ describe('plugin-meetings', () => {
88
88
 
89
89
  describe('#fetchMeetingInfo', () => {
90
90
  it('should fetch meeting info for the destination type', async () => {
91
+ const body = {meetingKey: '1234323'};
92
+ const requestResponse = {statusCode: 200, body};
93
+
91
94
  sinon.stub(MeetingInfoUtil, 'getDestinationType').returns(Promise.resolve({type: 'MEETING_ID', destination: '123456'}));
92
- sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve({meetingKey: '1234323'}));
95
+ sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(body));
96
+ webex.request.resolves(requestResponse);
93
97
 
94
- await meetingInfo.fetchMeetingInfo({
98
+ const result = await meetingInfo.fetchMeetingInfo({
95
99
  type: _MEETING_ID_,
96
100
  destination: '1234323'
97
101
  });
@@ -99,28 +103,39 @@ describe('plugin-meetings', () => {
99
103
  assert.calledWith(webex.request, {
100
104
  method: 'POST', service: 'webex-appapi-service', resource: 'meetingInfo', body: {meetingKey: '1234323'}
101
105
  });
106
+ assert.deepEqual(result, requestResponse);
102
107
 
103
108
  MeetingInfoUtil.getDestinationType.restore();
104
109
  MeetingInfoUtil.getRequestBody.restore();
105
110
  });
111
+
106
112
  it('should fetch meeting info for the personal meeting room type', async () => {
113
+ const body = {meetingKey: '1234323'};
114
+ const requestResponse = {statusCode: 200, body};
115
+
107
116
  sinon.stub(MeetingInfoUtil, 'getDestinationType').returns(Promise.resolve({type: 'MEETING_ID', destination: '123456'}));
108
- sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve({meetingKey: '1234323'}));
117
+ sinon.stub(MeetingInfoUtil, 'getRequestBody').returns(Promise.resolve(body));
118
+ webex.request.resolves(requestResponse);
109
119
 
110
- await meetingInfo.fetchMeetingInfo({
120
+ const result = await meetingInfo.fetchMeetingInfo({
111
121
  type: _PERSONAL_ROOM_
112
122
  });
113
123
 
114
124
  assert.calledWith(webex.request, {
115
125
  method: 'POST', service: 'webex-appapi-service', resource: 'meetingInfo', body: {meetingKey: '1234323'}
116
126
  });
127
+ assert.deepEqual(result, requestResponse);
117
128
 
118
129
  MeetingInfoUtil.getDestinationType.restore();
119
130
  MeetingInfoUtil.getRequestBody.restore();
120
131
  });
121
132
 
122
133
  it('should fetch meeting info with provided password and captcha code', async () => {
123
- await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
134
+ const requestResponse = {statusCode: 200, body: {meetingKey: '1234323'}};
135
+
136
+ webex.request.resolves(requestResponse);
137
+
138
+ const result = await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
124
139
 
125
140
  assert.calledWith(webex.request, {
126
141
  method: 'POST',
@@ -134,6 +149,7 @@ describe('plugin-meetings', () => {
134
149
  captchaVerifyCode: 'aabbcc11'
135
150
  }
136
151
  });
152
+ assert.deepEqual(result, requestResponse);
137
153
  assert(Metrics.sendBehavioralMetric.calledOnce);
138
154
  assert.calledWith(
139
155
  Metrics.sendBehavioralMetric,