Skip to content

Commit 9265407

Browse files
maxwellmooney13PureCloud Jenkinssverity77Steve VININDevEvangelists
authored
Release v2.5.1 (#109)
* Prep v2.4.5 and merging master back into develop * no-jira nullify active conversationId upon disconnect (#100) * no-jira nullify active conversationId upon disconnect * no-jira added in unit test coverage for new line * CyberAcoustics first pull request (#94) * CyberAcoustics first pull request * Fixed various codacy warnings * fixed more codacy warnings * Fixed even more codacy warnings * More codacy warnings fixed. There are some bogus warnings which will not be addressed. * fixed bug: double notifications from device sometimes, added Timeout when requesting WebHIDPermissions * Added logging message to detect multiple EventListeners on device input reports. * added code to deal with occasional unstable button presses on the device. * no-jira tweaking gitignores and dependencies * no-jira code cleanup and removal of trailing spaces; updated unit tests --------- Co-authored-by: Steve V <sverity77@gmail.com> Co-authored-by: maxwellmooney13 <83785824+maxwellmooney13@users.noreply.github.com> * Updating to standard MIT license * Updating to standard MIT license * Updating to standard MIT license * Updating to standard MIT license * Updating to standard MIT license * Updating to standard MIT license * Updating to standard MIT license * [STREAM-4] Update team name and mailing list (#105) * HID - Filter with specific vendor and product id (#104) * HID - Request specific devices per vendor id * HID - Filter with selected productId if possible * Fixed string to hex issue * Fix when label does not contain the productId * [STREAM-44] Add `jiraProjectKey` (#107) * [STREAM-73] [STREAM-74] fixes for Jabra (constant webhid modal) and Sennheiser/EPOS (new call starts muted) (#108) * [STREAM-73] [STREAM-74] - added in fixes for Jabra and Sennheiser/EPOS issues that have come up recently; testing still required * [STREAM-73] [STREAM-74] - unit tests * [STREAM-73] [STREAM-74] - attempting dep fix * [STREAM-73] [STREAM-74] - adjust files for linting purposes * [STREAM-73] [STREAM-74] - addressing PR comments * 2.5.0 * 2.5.1 * [STREAM-73] [STREAM-74] - update changelog * no-jira release prep --------- Co-authored-by: PureCloud Jenkins <jenkins@genesys.com> Co-authored-by: sverity77 <fxworks@fxworks.com> Co-authored-by: Steve V <sverity77@gmail.com> Co-authored-by: Genesys Developer Evangelists <developerevangelists@genesys.com> Co-authored-by: Jon Hjelle <hjon@users.noreply.github.com> Co-authored-by: Angelos Fasoulis <22166661+Draginfable@users.noreply.github.com>
1 parent d8fb8df commit 9265407

16 files changed

+4405
-5035
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dist
66
coverage/
77
tmp/
88
softphone-vendor-headsets*.tgz
9+
.history/

Jenkinsfile

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ def getBuildType = {
2525

2626
webappPipeline {
2727
projectName = 'vendor-headsets'
28-
team = 'Genesys Client Media (WebRTC)'
29-
mailer = 'genesyscloud-client-media@genesys.com'
28+
team = 'Client Streaming and Signaling'
29+
jiraProjectKey = 'STREAM'
30+
mailer = 'GcMediaStreamSignal@genesys.com'
3031
chatGroupId = '763fcc91-e530-4ed7-b318-03f525a077f6'
3132

3233
nodeVersion = '14.x'

changelog.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ All notable changes to this project will be documented in this file.
33
The format is based on [Keepa Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

6-
# [Unreleased](https://github.com/purecloudlabs/softphone-vendor-headsets/compare/v2.5.0...HEAD)
6+
# [Unreleased](https://github.com/purecloudlabs/softphone-vendor-headsets/compare/v2.5.1...HEAD)
7+
8+
# [v2.5.1](https://github.com/purecloudlabs/softphone-vendor-headsets/compare/v2.5.0...v2.5.1)
9+
## Fixed
10+
* [STREAM-73](https://inindca.atlassian.net/browse/STREAM-73) - fix for issue where Sennheiser/EPOS devices maintained a mute state when starting a new call
11+
* [STREAM-74](https://inindca.atlassian.net/browse/STREAM-73) - fix for issue where the WebHID permission modal popped up constantly for Jabra
712

813
# [v2.5.0](https://github.com/purecloudlabs/softphone-vendor-headsets/compare/v2.4.5...v2.5.0)
914
## Added

package-lock.json

+1,465-1,896
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "softphone-vendor-headsets",
3-
"version": "2.5.0",
3+
"version": "2.5.1",
44
"author": "Genesys",
55
"license": "MIT",
66
"cjs": "dist/cjs/src/library/index.js",

react-app/package-lock.json

+2,770-3,094
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react-app/src/library/services/headset.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default class HeadsetService {
3030
cyberAcoustics: VendorImplementation;
3131
selectedImplementation: VendorImplementation;
3232
headsetEvents$: Observable<ConsumedHeadsetEvents>;
33-
33+
3434
private headsetConversationStates: { [conversationId: string]: HeadsetStateRecord } = {};
3535
private _headsetEvents$: Subject<ConsumedHeadsetEvents>;
3636
private logger: any;

react-app/src/library/services/vendor-implementations/CyberAcoustics/CyberAcoustics.ts

+3
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,14 @@ export default class CyberAcousticsService extends VendorImplementation {
163163
const HIDPermissionTimeout = setTimeout(reject, 30000);
164164
this.requestWebHidPermissions(async () => {
165165
this.logger.debug("Requesting web HID permissions");
166+
const productId = this.deductProductId(originalDeviceLabel);
167+
166168
const devList = await (window.navigator as any).hid.requestDevice({
167169
filters: [
168170
{
169171
vendorId: 0x3391,
170172
//vendorId: 0x046D,
173+
productId: productId || undefined
171174
},
172175
],
173176
});

react-app/src/library/services/vendor-implementations/jabra/jabra.test.ts

+45-24
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,9 @@ const initializeSdk = async (subject?: Subject<IDevice[]>) => {
6363

6464
describe('JabraService', () => {
6565
let jabraService: JabraService;
66-
Object.defineProperty(window.navigator, 'hid', {
67-
get: () => ({
68-
getDevices: () => {
69-
return [];
70-
},
71-
}),
72-
});
66+
(window.navigator as any) = { ...(window.navigator as any), hid: {
67+
getDevices: jest.fn().mockResolvedValue([])
68+
} };
7369
Object.defineProperty(window.navigator, 'locks', { get: () => ({}) });
7470
(window as any).BroadcastChannel = BroadcastChannel;
7571

@@ -163,6 +159,7 @@ describe('JabraService', () => {
163159
it('should connect with previouslyConnectedDevice', async () => {
164160
const statusChangeSpy = jest.spyOn(jabraService, 'changeConnectionStatus');
165161
jest.spyOn(jabraService, 'getPreviouslyConnectedDevice').mockResolvedValue(mockDevice2 as any);
162+
jest.spyOn(jabraService, 'deviceHasPermissions').mockResolvedValue(true);
166163

167164
const callControl = createMockCallControl(new Subject<ICallControlSignal>().asObservable());
168165
jest
@@ -173,10 +170,36 @@ describe('JabraService', () => {
173170
},
174171
} as any);
175172

173+
176174
await jabraService.connect(mockDevice2.name);
177175
expect(statusChangeSpy).toHaveBeenCalledWith({ isConnected: true, isConnecting: false });
178176
});
179177

178+
it('should attempt to connect with previouslyConnectedDevice but timeout', async () => {
179+
const statusChangeSpy = jest.spyOn(jabraService, 'changeConnectionStatus');
180+
const resetHeadsetSpy = jest.spyOn(jabraService, 'resetHeadsetState');
181+
const processEventsSpy = jest.spyOn(jabraService, '_processEvents');
182+
const callControl = createMockCallControl(new Subject<ICallControlSignal>().asObservable());
183+
jest
184+
.spyOn(jabraService, 'createCallControlFactory')
185+
.mockReturnValue({
186+
createCallControl: async () => {
187+
return callControl;
188+
},
189+
} as any);
190+
jest.useFakeTimers();
191+
jest.spyOn(jabraService,'deviceHasPermissions').mockResolvedValue(true);
192+
await jabraService.connect(mockDevice2.name);
193+
194+
await flushPromises();
195+
196+
jest.advanceTimersByTime(15005);
197+
198+
expect(statusChangeSpy).toHaveBeenCalledWith({ isConnected: false, isConnecting: false });
199+
expect(resetHeadsetSpy).not.toHaveBeenCalled();
200+
expect(processEventsSpy).not.toHaveBeenCalled();
201+
});
202+
180203
it('should connect with webhidRequest', async () => {
181204
const statusChangeSpy = jest.spyOn(jabraService, 'changeConnectionStatus');
182205
jest.spyOn(jabraService, 'getPreviouslyConnectedDevice').mockResolvedValue(null);
@@ -1379,7 +1402,21 @@ describe('JabraService', () => {
13791402
await expect(devicePromise).rejects.toThrow('random error');
13801403
});
13811404
});
1382-
1405+
1406+
describe('deviceHasPermissions', () => {
1407+
Object.defineProperty(window.navigator, 'hid', {
1408+
get: () => ({
1409+
getDevices: () => { return [{ productName: 'test-device' } as any]; }
1410+
})
1411+
});
1412+
it('should return true if passed in label exists in getDevices', async () => {
1413+
const deviceHasPermissions = await jabraService.deviceHasPermissions('test-device');
1414+
await flushPromises();
1415+
1416+
expect(deviceHasPermissions).toBe(true);
1417+
});
1418+
});
1419+
13831420
describe('getPreviouslyConnectedDevice', () => {
13841421
afterEach(() => {
13851422
jest.useRealTimers();
@@ -1425,22 +1462,6 @@ describe('JabraService', () => {
14251462
expect(device).toBeNull();
14261463
});
14271464

1428-
it('should timeout after 3 seconds', async () => {
1429-
jest.useFakeTimers();
1430-
1431-
const sub = new BehaviorSubject([]);
1432-
jabraService.jabraSdk = await initializeSdk(sub as any) as any;
1433-
1434-
const devicePromise = jabraService.getPreviouslyConnectedDevice(mockDevice2.name);
1435-
1436-
await flushPromises();
1437-
1438-
jest.advanceTimersByTime(3100);
1439-
1440-
const device = await devicePromise;
1441-
expect(device).toBeFalsy();
1442-
});
1443-
14441465
it('should log random error', async () => {
14451466
const sub = new BehaviorSubject([]);
14461467
jabraService.jabraSdk = await initializeSdk(sub as any) as any;

react-app/src/library/services/vendor-implementations/jabra/jabra.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,16 @@ export default class JabraService extends VendorImplementation {
347347

348348
this._deviceInfo = null;
349349

350-
let selectedDevice = await this.getPreviouslyConnectedDevice(deviceLabel);
351-
if (!selectedDevice) {
350+
let selectedDevice;
351+
if (await this.deviceHasPermissions(deviceLabel)) {
352+
selectedDevice = await this.getPreviouslyConnectedDevice(deviceLabel);
353+
354+
if (!selectedDevice) {
355+
console.warn('Unable to find appropriate device. Setting state to "Not Running" to allow a retry"', deviceLabel);
356+
this.changeConnectionStatus({ isConnected: false, isConnecting: false });
357+
return;
358+
}
359+
} else {
352360
try {
353361
selectedDevice = await this.getDeviceFromWebhid(deviceLabel);
354362
} catch (e) {
@@ -370,16 +378,26 @@ export default class JabraService extends VendorImplementation {
370378
this.changeConnectionStatus({ isConnected: true, isConnecting: false });
371379
}
372380

381+
async deviceHasPermissions (deviceLabel: string): Promise<boolean> {
382+
const allowedHIDDevices = await (window.navigator as any).hid.getDevices();
383+
let deviceFound = false;
384+
allowedHIDDevices.forEach(device => {
385+
if (deviceLabel.includes(device?.productName?.toLowerCase())) {
386+
deviceFound = true;
387+
}
388+
});
389+
return deviceFound;
390+
}
391+
373392
async getPreviouslyConnectedDevice (deviceLabel: string): Promise<IDevice> {
374-
// we want to wait for up to 2 events or timeout after 2 seconds
375393
const waitForDevice: Observable<IDevice> = this.jabraSdk.deviceList.pipe(
376394
defaultIfEmpty(null),
377395
first((devices: IDevice[]) => !!devices.length),
378396
map((devices: IDevice[]) =>
379397
devices.find((device) => this.isDeviceInList(device, deviceLabel))
380398
),
381399
filter((device) => !!device),
382-
timeout(3000)
400+
timeout(15000)
383401
);
384402

385403
return firstValueFrom(waitForDevice).catch((err) => {
@@ -445,7 +463,7 @@ export default class JabraService extends VendorImplementation {
445463
this.callControl.releaseCallLock();
446464
} catch (e) {
447465
this.logger.warn('Failed to takeCallLock in order to resetHeadsetState. Ignoring reset.');
448-
}
466+
}
449467
}
450468

451469
async disconnect (): Promise<void> {

react-app/src/library/services/vendor-implementations/sennheiser/sennheiser.test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,45 @@ describe('SennheiserService', () => {
383383
EventType: SennheiserEventTypes.Request,
384384
CallID: conversationId
385385
};
386+
const expectedHoldPayload: SennheiserPayload = {
387+
Event: SennheiserEvents.Resume,
388+
EventType: SennheiserEventTypes.Request
389+
};
390+
const expectedMutePayload: SennheiserPayload = {
391+
Event: SennheiserEvents.UnmuteFromApp,
392+
EventType: SennheiserEventTypes.Request
393+
};
386394

387-
await sennheiserService.endCall(conversationId);
395+
await sennheiserService.endCall(conversationId, true);
388396

389397
expect(sennheiserService._sendMessage).toHaveBeenCalledWith(expectedPayload);
398+
expect(sennheiserService._sendMessage).not.toHaveBeenCalledWith(expectedHoldPayload);
399+
expect(sennheiserService._sendMessage).not.toHaveBeenCalledWith(expectedMutePayload);
390400
});
401+
402+
it('should reset state for mute and hold when there are no other active calls', async () => {
403+
jest.spyOn(sennheiserService, '_sendMessage');
404+
const conversationId = '23f897b';
405+
const expectedHoldPayload: SennheiserPayload = {
406+
Event: SennheiserEvents.Resume,
407+
EventType: SennheiserEventTypes.Request
408+
};
409+
const expectedMutePayload: SennheiserPayload = {
410+
Event: SennheiserEvents.UnmuteFromApp,
411+
EventType: SennheiserEventTypes.Request
412+
};
413+
const expectedEndPayload: SennheiserPayload = {
414+
Event: SennheiserEvents.CallEnded,
415+
EventType: SennheiserEventTypes.Request,
416+
CallID: conversationId
417+
};
418+
419+
await sennheiserService.endCall(conversationId, false);
420+
421+
expect(sennheiserService._sendMessage).toHaveBeenCalledWith(expectedHoldPayload);
422+
expect(sennheiserService._sendMessage).toHaveBeenCalledWith(expectedMutePayload);
423+
expect(sennheiserService._sendMessage).toHaveBeenCalledWith(expectedEndPayload);
424+
})
391425
});
392426

393427
describe('_handleMessage', () => {

react-app/src/library/services/vendor-implementations/sennheiser/sennheiser.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,22 @@ export default class SennheiserService extends VendorImplementation {
189189
return Promise.resolve();
190190
}
191191

192-
endCall (conversationId: string): Promise<void> {
192+
endCall (conversationId: string, hasOtherActiveCalls?: boolean): Promise<void> {
193+
if (!hasOtherActiveCalls) {
194+
this._sendMessage({
195+
Event: SennheiserEvents.Resume,
196+
EventType: SennheiserEventTypes.Request
197+
});
198+
this._sendMessage({
199+
Event: SennheiserEvents.UnmuteFromApp,
200+
EventType: SennheiserEventTypes.Request,
201+
});
202+
}
193203
this._sendMessage({
194204
Event: SennheiserEvents.CallEnded,
195205
EventType: SennheiserEventTypes.Request,
196206
CallID: conversationId,
197207
});
198-
199208
return Promise.resolve();
200209
}
201210

@@ -237,7 +246,7 @@ export default class SennheiserService extends VendorImplementation {
237246
if (!this.isConnected || this.isConnecting) {
238247
this.changeConnectionStatus({ isConnected: true, isConnecting: false });
239248
}
240-
249+
241250
this._sendMessage({
242251
Event: SennheiserEvents.SystemInformation,
243252
EventType: SennheiserEventTypes.Request,
@@ -285,6 +294,10 @@ export default class SennheiserService extends VendorImplementation {
285294
break;
286295
case SennheiserEvents.CallEnded:
287296
if (payload.EventType === SennheiserEventTypes.Notification) {
297+
this._sendMessage({
298+
Event: SennheiserEvents.UnmuteFromApp,
299+
EventType: SennheiserEventTypes.Request,
300+
});
288301
this.deviceEndedCall({ name: payload.Event, conversationId });
289302
}
290303
break;

react-app/src/library/services/vendor-implementations/vbet/vbet.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isCefHosted } from '../../../utils';
66

77
const HEADSET_USAGE = 0x0005;
88
const HEADSET_USAGE_PAGE = 0x000b;
9+
const VENDOR_ID = 0x340b;
910
const BT100USeries = [0x0001];
1011
const CMEDIASeries = [0x0020, 0x0022];
1112
const DECTSeries = [0x0014];
@@ -110,7 +111,8 @@ export default class VBetService extends VendorImplementation {
110111
this.activeDevice = await new Promise((resolve, reject) => {
111112
const waiter = setTimeout(reject, 30000);
112113
this.requestWebHidPermissions(async () => {
113-
const filters = [{ usage: HEADSET_USAGE, usagePage: HEADSET_USAGE_PAGE }];
114+
const productId = this.deductProductId(originalDeviceLabel);
115+
const filters = [{ usage: HEADSET_USAGE, usagePage: HEADSET_USAGE_PAGE, vendorId: VENDOR_ID, productId: productId || undefined }];
114116
await (window.navigator as any).hid.requestDevice({ filters });
115117
clearTimeout(waiter);
116118
const deviceList = await (window.navigator as any).hid.getDevices();
@@ -143,9 +145,9 @@ export default class VBetService extends VendorImplementation {
143145
}
144146
}
145147

146-
148+
147149
!this.activeDevice.opened && await this.activeDevice.open();
148-
150+
149151

150152
this.logger.debug(`device input reportId ${this.inputReportReportId}`);
151153
this.activeDevice.addEventListener('inputreport', (event: PartialInputReportEvent) => {
@@ -214,7 +216,7 @@ export default class VBetService extends VendorImplementation {
214216
await this.setMuteFromDevice(!this.isMuted);
215217
break;
216218
}
217-
}
219+
}
218220
if (this.activeDevice.productId >= 0x0040 && this.activeDevice.productId <= 0x0083) {
219221
switch (value) {
220222
case 0x20:
@@ -323,7 +325,7 @@ export default class VBetService extends VendorImplementation {
323325
await this.sendOpToDevice('onHook');
324326
} else {
325327
this.logger.error('no call to be ended');
326-
}
328+
}
327329
}
328330
}
329331

react-app/src/library/services/vendor-implementations/vendor-implementation.ts

+12
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,16 @@ export abstract class VendorImplementation extends (EventEmitter as { new(): Str
138138
this.isConnecting = headsetState.isConnecting;
139139
this.emitEvent('deviceConnectionStatusChanged', { currentVendor: this, ...headsetState });
140140
}
141+
142+
/**
143+
* Try to deduct the product id based on the label.
144+
* Making the assumption that the label will end with (vendorid:productid).
145+
*
146+
* @param selectedMicLabel
147+
* @returns The product id if matched or null.
148+
*/
149+
deductProductId (selectedMicLabel: string) : number {
150+
const match = selectedMicLabel?.match(/\((\w+):(\w+)\)$/);
151+
return match ? parseInt(match[2], 16) : null;
152+
}
141153
}

0 commit comments

Comments
 (0)