@webex/plugin-meetings 2.13.0 → 2.14.2
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/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/locus-info/controlsUtils.js +12 -2
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +14 -0
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/effectsState.js +328 -0
- package/dist/meeting/effectsState.js.map +1 -0
- package/dist/meeting/index.js +102 -195
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +6 -10
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.js +1 -0
- package/src/locus-info/controlsUtils.js +11 -0
- package/src/locus-info/index.js +16 -0
- package/src/meeting/effectsState.js +206 -0
- package/src/meeting/index.js +83 -98
- package/src/meeting-info/meeting-info-v2.js +1 -4
- package/test/unit/spec/locus-info/index.js +34 -0
- package/test/unit/spec/meeting/effectsState.js +292 -0
- package/test/unit/spec/meeting/index.js +42 -200
- package/test/unit/spec/meeting-info/meetinginfov2.js +13 -2
package/src/meeting/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import Media from '../media';
|
|
|
16
16
|
import MediaProperties from '../media/properties';
|
|
17
17
|
import MeetingStateMachine from '../meeting/state';
|
|
18
18
|
import createMuteState from '../meeting/muteState';
|
|
19
|
+
import createEffectsState from '../meeting/effectsState';
|
|
19
20
|
import LocusInfo from '../locus-info';
|
|
20
21
|
import PeerConnectionManager from '../peer-connection-manager';
|
|
21
22
|
import Metrics from '../metrics';
|
|
@@ -531,6 +532,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
531
532
|
* @memberof Meeting
|
|
532
533
|
*/
|
|
533
534
|
this.video = null;
|
|
535
|
+
/**
|
|
536
|
+
* created later
|
|
537
|
+
* @instance
|
|
538
|
+
* @type {EffectsState}
|
|
539
|
+
* @private
|
|
540
|
+
* @memberof Meeting
|
|
541
|
+
*/
|
|
542
|
+
this.effects = null;
|
|
534
543
|
/**
|
|
535
544
|
* @instance
|
|
536
545
|
* @type {MeetingStateMachine}
|
|
@@ -915,14 +924,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
915
924
|
*/
|
|
916
925
|
this.meetingInfoFailureReason = undefined;
|
|
917
926
|
|
|
918
|
-
/**
|
|
919
|
-
* Indicates the current status of BNR in the meeting
|
|
920
|
-
* @type {BNR_STATUS}
|
|
921
|
-
* @private
|
|
922
|
-
* @memberof Meeting
|
|
923
|
-
*/
|
|
924
|
-
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
925
|
-
|
|
926
927
|
this.setUpLocusInfoListeners();
|
|
927
928
|
this.locusInfo.init(attrs.locus ? attrs.locus : {});
|
|
928
929
|
this.hasJoinedOnce = false;
|
|
@@ -1592,6 +1593,24 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1592
1593
|
{meetingContainerUrl}
|
|
1593
1594
|
);
|
|
1594
1595
|
});
|
|
1596
|
+
|
|
1597
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIBE_UPDATED,
|
|
1598
|
+
({caption, transcribing}) => {
|
|
1599
|
+
if (transcribing && this.transcription && this.config.receiveTranscription) {
|
|
1600
|
+
this.receiveTranscription();
|
|
1601
|
+
}
|
|
1602
|
+
else if (!transcribing && this.transcription) {
|
|
1603
|
+
Trigger.trigger(
|
|
1604
|
+
this,
|
|
1605
|
+
{
|
|
1606
|
+
file: 'meeting/index',
|
|
1607
|
+
function: 'setupLocusControlsListener'
|
|
1608
|
+
},
|
|
1609
|
+
EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION,
|
|
1610
|
+
{caption, transcribing}
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1595
1614
|
}
|
|
1596
1615
|
|
|
1597
1616
|
/**
|
|
@@ -3396,7 +3415,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3396
3415
|
* @throws TranscriptionNotSupportedError
|
|
3397
3416
|
*/
|
|
3398
3417
|
isTranscriptionSupported() {
|
|
3399
|
-
if (this.
|
|
3418
|
+
if (this.locusInfo.controls.transcribe?.transcribing) {
|
|
3400
3419
|
return true;
|
|
3401
3420
|
}
|
|
3402
3421
|
|
|
@@ -4495,7 +4514,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4495
4514
|
if (!this.canUpdateMedia()) {
|
|
4496
4515
|
return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
|
|
4497
4516
|
}
|
|
4498
|
-
const {
|
|
4517
|
+
const {
|
|
4518
|
+
sendAudio, receiveAudio, stream, bnrEnabled
|
|
4519
|
+
} = options;
|
|
4499
4520
|
const {audioTransceiver} = this.mediaProperties.peerConnection;
|
|
4500
4521
|
let track = MeetingUtil.getTrack(stream).audioTrack;
|
|
4501
4522
|
|
|
@@ -4503,9 +4524,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4503
4524
|
return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
|
|
4504
4525
|
}
|
|
4505
4526
|
|
|
4506
|
-
if (sendAudio && !this.isAudioMuted() && (
|
|
4527
|
+
if (sendAudio && !this.isAudioMuted() && (bnrEnabled === BNR_STATUS.ENABLED || bnrEnabled === BNR_STATUS.SHOULD_ENABLE)) {
|
|
4528
|
+
LoggerProxy.logger.info('Meeting:index#updateAudio. Calling WebRTC enable bnr method');
|
|
4507
4529
|
track = await this.internal_enableBNR(track);
|
|
4508
|
-
|
|
4530
|
+
LoggerProxy.logger.info('Meeting:index#updateAudio. WebRTC enable bnr request completed');
|
|
4509
4531
|
}
|
|
4510
4532
|
|
|
4511
4533
|
return MeetingUtil.validateOptions({sendAudio, localStream: stream})
|
|
@@ -5771,7 +5793,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5771
5793
|
* @memberof Meeting
|
|
5772
5794
|
*/
|
|
5773
5795
|
isBnrEnabled() {
|
|
5774
|
-
return this.
|
|
5796
|
+
return this.effects && this.effects.isBnrEnabled();
|
|
5775
5797
|
}
|
|
5776
5798
|
|
|
5777
5799
|
/**
|
|
@@ -5797,109 +5819,72 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5797
5819
|
}
|
|
5798
5820
|
|
|
5799
5821
|
/**
|
|
5800
|
-
*
|
|
5801
|
-
* @returns {Promise
|
|
5822
|
+
* Enable the audio track with BNR for a meeting
|
|
5823
|
+
* @returns {Promise} resolves the data from enable bnr or rejects if there is no audio or audio is muted
|
|
5802
5824
|
* @public
|
|
5803
5825
|
* @memberof Meeting
|
|
5804
5826
|
*/
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
try {
|
|
5810
|
-
if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5811
|
-
throw new Error("Meeting doesn't have an audioTrack attached");
|
|
5812
|
-
}
|
|
5813
|
-
else if (this.isAudioMuted()) {
|
|
5814
|
-
throw new Error('Cannot enable BNR while meeting is muted');
|
|
5815
|
-
}
|
|
5827
|
+
enableBNR() {
|
|
5828
|
+
if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5829
|
+
return Promise.reject(new Error("Meeting doesn't have an audioTrack attached"));
|
|
5830
|
+
}
|
|
5816
5831
|
|
|
5832
|
+
if (this.isAudioMuted()) {
|
|
5833
|
+
return Promise.reject(new Error('Cannot enable BNR while meeting is muted'));
|
|
5834
|
+
}
|
|
5817
5835
|
|
|
5818
|
-
|
|
5836
|
+
this.effects = this.effects || createEffectsState('BNR');
|
|
5819
5837
|
|
|
5820
|
-
|
|
5838
|
+
const LOG_HEADER = 'Meeting:index#enableBNR -->';
|
|
5821
5839
|
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
receiveAudio: this.mediaProperties.mediaDirection.receiveAudio,
|
|
5826
|
-
stream: audioStream
|
|
5827
|
-
});
|
|
5828
|
-
this.bnrStatus = BNR_STATUS.ENABLED;
|
|
5829
|
-
isSuccess = true;
|
|
5830
|
-
Metrics.sendBehavioralMetric(
|
|
5831
|
-
BEHAVIORAL_METRICS.ENABLE_BNR_SUCCESS,
|
|
5832
|
-
);
|
|
5833
|
-
}
|
|
5834
|
-
catch (error) {
|
|
5835
|
-
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
5836
|
-
Metrics.sendBehavioralMetric(
|
|
5837
|
-
BEHAVIORAL_METRICS.ENABLE_BNR_FAILURE,
|
|
5838
|
-
{
|
|
5839
|
-
reason: error.message,
|
|
5840
|
-
stack: error.stack
|
|
5841
|
-
}
|
|
5842
|
-
);
|
|
5843
|
-
LoggerProxy.logger.error('Meeting:index#enableBNR.', error);
|
|
5844
|
-
throw error;
|
|
5845
|
-
}
|
|
5840
|
+
return logRequest(this.effects.handleClientRequest(true, this)
|
|
5841
|
+
.then((res) => {
|
|
5842
|
+
LoggerProxy.logger.info('Meeting:index#enableBNR. Enable bnr completed');
|
|
5846
5843
|
|
|
5847
|
-
|
|
5844
|
+
return res;
|
|
5845
|
+
})
|
|
5846
|
+
.catch((error) => {
|
|
5847
|
+
throw error;
|
|
5848
|
+
}),
|
|
5849
|
+
{
|
|
5850
|
+
header: `${LOG_HEADER} enable bnr`,
|
|
5851
|
+
success: `${LOG_HEADER} enable bnr success`,
|
|
5852
|
+
failure: `${LOG_HEADER} enable bnr failure, `
|
|
5853
|
+
});
|
|
5848
5854
|
}
|
|
5849
5855
|
|
|
5850
5856
|
/**
|
|
5851
|
-
*
|
|
5852
|
-
* @returns {Promise
|
|
5857
|
+
* Disable the BNR for an audio track
|
|
5858
|
+
* @returns {Promise} resolves the data from disable bnr or rejects if there is no audio set
|
|
5853
5859
|
* @public
|
|
5854
5860
|
* @memberof Meeting
|
|
5855
5861
|
*/
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
try {
|
|
5861
|
-
if (!this.isBnrEnabled()) {
|
|
5862
|
-
throw new Error('Can not disable as BNR is not enabled');
|
|
5863
|
-
}
|
|
5864
|
-
else if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5865
|
-
throw new Error("Meeting doesn't have an audioTrack attached");
|
|
5866
|
-
}
|
|
5867
|
-
const audioTrack = WebRTCMedia.Effects.BNR.disableBNR(this.mediaProperties.audioTrack);
|
|
5868
|
-
const audioStream = MediaUtil.createMediaStream([audioTrack]);
|
|
5869
|
-
|
|
5870
|
-
LoggerProxy.logger.info('Meeting:index#disableBNR. Raw media track obtained from WebRTC & sent to updateAudio');
|
|
5871
|
-
|
|
5872
|
-
this.bnrStatus = BNR_STATUS.SHOULD_DISABLE;
|
|
5873
|
-
|
|
5874
|
-
await this.updateAudio({
|
|
5875
|
-
sendAudio: true,
|
|
5876
|
-
receiveAudio: this.mediaProperties.mediaDirection.receiveAudio,
|
|
5877
|
-
stream: audioStream
|
|
5878
|
-
});
|
|
5879
|
-
|
|
5880
|
-
this.bnrStatus = BNR_STATUS.NOT_ENABLED;
|
|
5881
|
-
|
|
5882
|
-
isSuccess = true;
|
|
5862
|
+
disableBNR() {
|
|
5863
|
+
if (typeof this.mediaProperties === 'undefined' || typeof this.mediaProperties.audioTrack === 'undefined') {
|
|
5864
|
+
return Promise.reject(new Error("Meeting doesn't have an audioTrack attached"));
|
|
5865
|
+
}
|
|
5883
5866
|
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
);
|
|
5867
|
+
if (!this.isBnrEnabled()) {
|
|
5868
|
+
return Promise.reject(new Error('Can not disable as BNR is not enabled'));
|
|
5887
5869
|
}
|
|
5888
|
-
catch (error) {
|
|
5889
|
-
this.bnrStatus = BNR_STATUS.ENABLED;
|
|
5890
|
-
LoggerProxy.logger.error(`Meeting:index#disableBNR. ${error}`);
|
|
5891
5870
|
|
|
5892
|
-
|
|
5893
|
-
BEHAVIORAL_METRICS.DISABLE_BNR_FAILURE,
|
|
5894
|
-
{
|
|
5895
|
-
reason: error.message,
|
|
5896
|
-
stack: error.stack
|
|
5897
|
-
}
|
|
5898
|
-
);
|
|
5871
|
+
this.effects = this.effects || createEffectsState('BNR');
|
|
5899
5872
|
|
|
5900
|
-
|
|
5901
|
-
|
|
5873
|
+
const LOG_HEADER = 'Meeting:index#disableBNR -->';
|
|
5874
|
+
|
|
5875
|
+
return logRequest(this.effects.handleClientRequest(false, this)
|
|
5876
|
+
.then((res) => {
|
|
5877
|
+
LoggerProxy.logger.info('Meeting:index#disableBNR. Disable bnr completed');
|
|
5902
5878
|
|
|
5903
|
-
|
|
5879
|
+
return res;
|
|
5880
|
+
})
|
|
5881
|
+
.catch((error) => {
|
|
5882
|
+
throw error;
|
|
5883
|
+
}),
|
|
5884
|
+
{
|
|
5885
|
+
header: `${LOG_HEADER} disable bnr`,
|
|
5886
|
+
success: `${LOG_HEADER} disable bnr success`,
|
|
5887
|
+
failure: `${LOG_HEADER} disable bnr failure, `
|
|
5888
|
+
});
|
|
5904
5889
|
}
|
|
5905
5890
|
}
|
|
@@ -151,10 +151,7 @@ export default class MeetingInfoV2 {
|
|
|
151
151
|
method: HTTP_VERBS.POST,
|
|
152
152
|
uri,
|
|
153
153
|
body
|
|
154
|
-
})
|
|
155
|
-
.catch((err) => {
|
|
156
|
-
throw new MeetingInfoV2AdhocMeetingError(err.body?.code, err.body?.message);
|
|
157
|
-
});
|
|
154
|
+
});
|
|
158
155
|
})
|
|
159
156
|
.catch((err) => {
|
|
160
157
|
Metrics.sendBehavioralMetric(
|
|
@@ -255,6 +255,40 @@ describe('plugin-meetings', () => {
|
|
|
255
255
|
});
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it('should update the transcript state', () => {
|
|
259
|
+
locusInfo.emitScoped = sinon.stub();
|
|
260
|
+
locusInfo.controls = {
|
|
261
|
+
lock: {},
|
|
262
|
+
meetingFull: {},
|
|
263
|
+
record: {
|
|
264
|
+
recording: false,
|
|
265
|
+
paused: true,
|
|
266
|
+
meta: {
|
|
267
|
+
lastModified: 'TODAY',
|
|
268
|
+
modifiedBy: 'George Kittle'
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
shareControl: {},
|
|
272
|
+
transcribe: {
|
|
273
|
+
transcribing: false,
|
|
274
|
+
caption: false
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
newControls.transcribe.transcribing = true;
|
|
278
|
+
newControls.transcribe.caption = true;
|
|
279
|
+
|
|
280
|
+
locusInfo.updateControls(newControls);
|
|
281
|
+
|
|
282
|
+
assert.calledWith(locusInfo.emitScoped, {
|
|
283
|
+
file: 'locus-info',
|
|
284
|
+
function: 'updateControls'
|
|
285
|
+
},
|
|
286
|
+
LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIBE_UPDATED,
|
|
287
|
+
{
|
|
288
|
+
transcribing: true, caption: true
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
258
292
|
it('should update the meetingContainerURL from null', () => {
|
|
259
293
|
locusInfo.controls = {
|
|
260
294
|
meetingContainer: {meetingContainerUrl: null},
|
|
@@ -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
|
+
});
|