@kandy-io/kandy-hid
v2.3.0
Published
abstraction library for HID devices in Desktop and VDI
Downloads
5
Readme
Ribbon's WebRTC HID SDK
The HID SDK enables application developers to handle HID device call operations by abstracting device functions.
It is currently supported for use in Chrome-based Browsers (non-VDI) and Electron-based WebRTC applications running on Windows and Mac desktops and VDI environments. Note that use in VDI environments requires the Kandy VDI Toolkit.
The following Jabra headsets and configurations are currently supported, with support for other devices and vendors possible.
| Device Make / Model | Desktop | VDI eLux | VDI Windows | VDI Mac | Browser | Firmware | | :-----------------: | :-----: | :------: | :---------: | :-----: | :------: | :-----------: | | Jabra Engage 65 | ☑ | ☑ | ☑ | ☑ | ☑ | 2.0.5, 3.4.1 | | Jabra Engage 50 | ☑ | ☑ | ☑ | ☑ | ☑ | 1.24.0, 2.3.1 | | Jabra Engage 50 II | ☑ | ☑ | ☑ | ☑ | ☑ | 3.7.2 | | Jabra Speak 750 | ☑ | ☑ | ☑ | ☑ | ☑ | 2.24.0 | | Jabra Evolve2 40 | ☑ | ☑ | ☑ | ☑ | ☑ | 1.19.0 |
As of version 2.0.0, this SDK is to be used in Electron's Renderer process, rather than the Main process. It's necessary to require
the SDK in Electron's Main Process to maintain backwards-compatibility for configurations where WebHID is not used. See Installation and initializeHIDDevices
below.
Refer to the README for version 1.x for instructions on using 1.x versions of the SDK in Electron's main process.
Definitions
Legacy eLux/VDI is defined as an eLux VDI environment where the version of Kandy Distant Driver for VDI is <1.6.0. Therefore, non-legacy VDI referenced below is any release of Mac or Windows VDI, and eLux VDI where the version of Kandy Distant Driver for VDI is 1.6.0 or greater.
Installation
The SDK is shipped as a .tgz package that can be retrieved from NPM:
npm install @kandy-io/kandy-hid
Electron Main Process
require
the top-level library
require('@kandy-io/kandy-hid')
Electron Renderer Process / Browser
Import initializeHIDDevices
from '@kandy-io/kandy-hid/local'.
import { initializeHIDDevices } from '@kandy-io/kandy-hid/local'
Logging
Logs are not written to file or visible on the console by default.
Electron Main
To view logs generated by this SDK in Electron's main process (in desktop or legacy VDI), in a bash window, do export DEBUG=kandy-hid
and then launch the app from the same bash window. Logs will be visible in the bash window.
Alternatively, the setLogger
API can be used to pass in a custom logger such as electron-log
. If setLogger
is used, the previous method to view logs in bash is disabled.
Electron Renderer / Browser
To make logs visible in the renderer process, open application DevTools, go to the Application tab, expand the LocalStorage list, select the application URL then add a new Key/Value pair with Key = debug
and Value = kandy-hid
. Close and re-open the app, then again open DevTools, go to the Console and enable Verbose logs.
Alternatively, a rendererLogs
flag can be set to true
and included in the object passed to initializeHIDDevices
. This will make logs visible in the DevTools console at info level. If the rendererLogs
method is used, the previous method is disabled.
API
All of the API's below are to be called from the Browser or Electron's Renderer process in the local app (as opposed to the remote) unless otherwise specified.
initializeHIDDevices([config])
Initializes the SDK with configuration parameters.
A config object is required in Electron VDI, optional otherwise.
{
mode: 'desktop' // valid values are 'desktop', 'browser' and 'VDI'; default is 'desktop' in electron, 'browser' in browser
driverInfo: {} // object returned by 'vchannel.getInfo()' in the Main process, see example below
useDriver: false // see Backwards Compatibility
rendererLogs: false // see Logging
}
Examples:
Initializing the local instance for Desktop or Browser
If your app is operating in a non-VDI environment, you can call initializeHIDDevices
as early as you like during your app's initialization. In those cases, mode
can be specified as 'desktop'
or 'browser'
, but is not required.
const kandyHID = initializeHIDDevices({ mode: 'desktop' })
Initializing the local instance for VDI
If your app is operating in a VDI environment, initialization of the SDK must be deferred until after the VDI channel has been opened. This is necessary because information about the remote client must be retrieved by calling vchannel.getInfo() and included in the configuration object passed in to initializeHIDDevices
as driverInfo
.
Example:
In your Electron Main process code, where the channel used by the main VDI session is opened:
const { ipcMain } = require('electron')
const vchannel = require('@distant/vchannel')
let driverInfo
async function openChannel() {
const channel = await vchannel.openVirtualChannel('RIBBON')
driverInfo = await channel.getInfo()
}
ipcMain.on('getDriverInfo', event => {
event.returnValue = driverInfo
})
In the Renderer process code, at the point where the local HID SDK will be initialized:
const mode = 'VDI'
// retrieve driverInfo from the Main process
const driverInfo = ipcRenderer.sendSync('getDriverInfo')
const kandyHID = initializeHIDDevices({ mode, driverInfo })
Returns an object that contains:
- An event emitter that emits
HIDFunctionRequest
events (replacingipcRenderer.on('HIDFunctionRequest', ...)
in 1.x). SeeHIDFunctionRequest
below. - All APIs that follow.
Initializing the Remote (non-legacy VDI only)
All that's required to initialize the remote is to call setChannel
with a valid communication object. See below.
setChannel(commsObject)
(non-legacy VDI only)
Must be called on both the local and remote sides in VDI. The commsObj
function parameter is an object that contains
- a
send
function that will send a message over the channel to the far end - a key called
receive
that has a value ofundefined
. The SDK will insert its receive function here. This is the same method used to initialize the KandyJS SDK.
Examples:
Local
On the local side, the setChannel
function is part of the object returned by initializeHIDDevices
.
const kandyHID = initializeHIDDevices({
mode: 'VDI',
driverInfo: ipcRenderer.sendSync('getDriverInfo')
})
kandyHID.setChannel({...})
Remote
On the remote side, setChannel
is imported directly from the remote library:
import { setChannel } from '@kandy-io/kandy-hid/remote'
Usage is the same in both local and remote:
const HIDChannel = {
send: message => {
yourSendFunction('hid', message)
},
receive: undefined
}
setChannel(HIDChannel)
// handle incoming messages from the far end
yourReceiveFunction(messageType, ...data) {
switch (messageType) {
case 'hid':
HIDChannel.receive(...data)
break
...
}
}
storeMainWindowID(id)
(desktop and legacy VDI only)
This SDK emits events in the Electron Renderer on the HIDFunctionRequest
event. In order to do this, it needs the id of the Electron renderer window that should receive these messages.
Example:
const electronRemote = require('electron').remote
kandyHID.storeMainWindowID(
elecronRemote.getCurrentWindow().id
)
isSupportedDevice(label)
Accepts a string
, typically the label component of a deviceInformation object (see selectHIDDevice).
Returns a boolean indicating whether a device is supported for use or not.
Example:
function selectMicrophone(deviceObject) {
const result = kandyHID.isSupportedDevice(deviceObject.label);
if (!result) {
console.log('unsupported device selected...')
}
kandyHID.selectHIDDevice('microphone', deviceObject);
}
This API will return true for supported devices listed in the introductory section and for 'Jabra Evolve 80'. It will return false for any other string. The device label passed in is compared against the device names as they appear in the introduction. The label must contain one of these names in order for this API to return true - it does not have to match exactly.
Examples:
kandyHID.isSupportedDevice('G/N Audio Jabra Speak 750 Mono'); // true
kandyHID.isSupportedDevice('Jabra Speak 750'); // true
kandyHID.isSupportedDevice('Jabra Speak 75'); // false
It's important that isSupportedDevice()
not be used to filter devices before selecting them via selectHIDDevice()
. For proper operation the SDK should be informed when device selections change, whether a supported HID device is selected or not. If your app initially selects a HID device for ringing, then later selects a different device for ringing, if you don't send the second device selection, the HID device will continue to ring unless you check/filter HID operations too. This SDK does that checking for you; just send all device selections into it, it'll take care of the rest.
selectHIDDevice(deviceType, deviceInformation)
Registers an association between a HID device (e.g. Jabra Engage 65) and a media device type (e.g. microphone).
Once the same device is selected for all 3 device types, a connection to the device will be established. That means, this API must be called by your app 3 times before a device can be used, once for each 'deviceType'
: 'microphone', 'speakers' and 'alert_speakers'.
The 'deviceInformation'
object will typically be a 'MediaDeviceInfo'
as returned by enumerateDevices
, but the only requirement is that it contain a label
property. The label
must match or contain one of the device model names before a device will be opened for use.
This API does not return anything, however calling it will cause the deviceSelectionChange
event to be emitted.
When the same device is selected for all device types, the deviceStateChange
event will be emitted indicating the device is open. When the selections no longer match, it'll be emitted again indicating the device is closed.
allowHIDDeviceOpens(bool)
Allows or disallows HID devices from being opened. True by default.
Devices should be closed before exiting an application; attempting to exit an app with an open device may cause issues. Depending on what your app does during shutdown, it may be necessary to prevent devices from being opened (after they've been closed) during app shutdown procedures.
Included for backwards-compatibility only. The SDK closes devices automatically during app exit.
invokeHIDFunction(operation)
Sends an instruction to the SDK to cause a specific state change on a device.
Operation (string
) may be one of:
- 'call_start': indicates that an outgoing call has started; causes the device to go offhook
- 'call_accept': indicates that an incoming ringing call has ben answered; causes the device to stop ringing and go offhook
- 'call_reject': indicates that an incoming ringing call has been rejected; stops device alerting / ringing
- 'call_end': indicates that an active call has ended; returns the device to default state
- 'call_mute': mutes the HID device. On some devices this causes visual or audible alerts
- 'call_unmute': unmutes the HID device
- 'start_ringing': causes the HID device to perform its alerting function (ringing, blinking, etc.)
- 'stop_ringing': causes the HID device to stop its alerting function. Note that sending any call state change operation will stop device alerting, so it's not necessary to call 'stop_ringing' when a call is answered or rejected
- 'call_hold': causes the HID device to perform a call hold action. On some devices this causes audible alerts or visible state changes
- 'call_resume': causes the HID device to exit its hold state
- 'offhook': causes the HID device to go offhook
- 'onhook': causes the HID device to go onhook
- 'reset': resets the device to default state
- 'calls_on_hold': informs the SDK when calls are put on hold in the app. See call swap documentation.
getVersion()
Returns the current version of the SDK as a string.
Events
HIDFunctionRequest(operation)
When a user performs an action on a HID device, the SDK will interpret the action based on previous state and send this interpretation to the app. For example, if the user presses the mute button during an active call, 'call_mute' will be emited to the app.
Operation will be one of the following:
- 'call_start': the device has gone offhook indicating an attempt to originate a call
- 'call_accept': the device has gone offhook while it was in ringing state, indicating an attempt to answer the call
- 'call_reject': an incoming ringing call has been rejected by the device
- 'call_end': the device has gone onhook, indicating an attempt to end the active call
- 'call_mute': the device's mute button was pushed, indicating an attempt to mute the active call
- 'call_unmute': the device's mute button was pushed while in the muted state, indicating an attempt to unmute the active call
- 'call_hold': the device's hold function sequence was invoked, indicating an attempt to put the active call on hold
- 'call_resume': the device's resume function sequence was invoked, indicating an attempt to resume/unhold the held call
- 'call_swap': the user has indicated the desire to swap between an active and a held call (1)
- 'device_error': (browser and VDI only) the SDK has detected a previously connected device has been disconnected (power loss or physical disconnection)
- 'channel_error': (legacy VDI eLux only) the SDK has detected a loss of communication with the Thin Client
These messages are emitted in the Electron renderer window identified by storeMainWindowID()
, or the current renderer process on the object returned by initializeHIDDevices()
.
The web app must have an handler to receive these events. Device-initiated events have identifier HIDFunctionRequest
.
const kandyHID = initializeHIDDevices()
kandyHID.on('HIDFunctionRequest', operation => {
switch (operation) {
case 'call_start':
// start a call in your app
break;
case 'call_mute':
// mute an active call in your app
break;
...
}
})
window.addEventListener('beforeunload', () => {
kandyHID.off('HIDFunctionRequest')
})
IMPORTANT you'll notice that the list of operations sent up to your app as a result of a user performing an action on the device are a subset of the operations your app sends via invokeHIDFunction()
. That is not coincidental. When this SDK notifies your app of a state change, it's important that you take whatever actions are necessary within your app (i.e. invoke your mute function), but also replay the operation back to the SDK to update the device's state1. This is necessary to keep your app, the SDK and the device in sync. Your app is in charge of updating device state via this SDK. The device will not change state, and this SDK will not modify device state, unless instructed to do so by your app (other than in error scenarios).
For example, if a user presses the mute button on a HID device during an active call, the device does not enter the mute state immediately; to complete a mute operation, your app should mute the active call and then, if successful, send a 'call_mute' instruction to mute the device. If for whatever reason your app fails to mute the active call, it should not replay the mute instruction, again in order to keep everything in sync.
1 Do not replay operations 'device_error', 'channel_error' or 'call_swap' back to the SDK. For a device-initiated call swap, your app should send 'call_hold' followed by 'call_resume'. See call swap documentation.
deviceSelectionChange
Each time that selectHIDDevice()
is called, a deviceSelectionChange
event will be emitted providing details on selected devices, for example:
{
selectedDevices: {
microphone: { label: 'Default - Jabra Speak 710 (0b0e:2475)', supported: true },
speakers: { label: 'Default - Jabra Speak 710 (0b0e:2475)', supported: true },
alert_speakers: { label: 'Default - Jabra Speak 710 (0b0e:2475)', supported: true }
},
availableDevices: {...}
}
(Note that availableDevices
may be empty until a device is opened for use.)
Error Parameter
Using a HID device in a browser requires that the user give the browser permission to access the device. When a device is selected for use, if the user has not previously granted access to the selected device, the browser will prompt the user for access. If access is granted, the device will be opened and a deviceStateChange
event will be emitted. If the user does not grant access, the deviceSelectionChange
event will be emitted with an error
parameter, indicating that the device has been properly selected, but is not available for use. When the error parameter is included in the emitted data, the only way to render the device usable is to re-select the device and manually grant permission. Therefore, when the error parameter is present, you must tell the user what to do via your UI.
Example:
kandyHID.on('deviceSelectionChange', details => {
if (details.error) {
alert('No access to selected device! Re-select device and grant access when prompted');
}
console.log('HID: deviceSelectionChange: ', details);
});
deviceStateChange
When selections for all device selections match for a given supported HID device, a connection to the device will be established, after which commands may be sent to the device and actions taken on the device will result in events being emitted into the parent appication. At this point the deviceStateChange
event will be emitted indicating the device is 'open'.
If the device is disconnected or device selections change such that they no longer match, the connection to the device will be closed. At this point the deviceStateChange
event will be emitted indicating the device is 'closed'.
Use Cases
Answering an incoming call
An incoming ringing call can be answered by:
- undocking the headset from the base (if present)
- pressing the MultiFunction button on the headset or base (if present)
- pressing the green Call Start/Answer button on the base or the Call button for single-button devices
- lowering the microphone boom (Evolve2 40 only, providing the device is not already on a call)
Any of these actions will cause 'call_accept' to be emitted. The device must be in a ringing state for an offhook action to be interpreted as 'call_accept'.
Answering an incoming call while on another call
Jabra Engage 50 II
- pressing and holding the Call Start / End button on the Link controller for 1-2 seconds
For all other devices, the device's call hold/resume action should be performed. See Hold/Resume below.
Originating an outgoing call
If the device goes offhook using any of the methods described above and is not in a ringing state, it will emit a 'call_start' operation on the HIDFunctionRequest event. It's then up to your app to take the appropriate action to start a call. If your app cannot successfully start a call in that case, it should send 'call_failure', followed by 'call_failure_finish' 1 second later to return the device to default state. Failure to do so may result in the device being left in the offhook state.
NOTE that the Engage 65 base must be in "Soft Phone Mode" prior to going offhook. This can be accomplished by pressing the MultiFunction button on the headset or the Call Answer button on the base for 1 second prior to removing the headset from the base. See the device's User Manual for more details.
kandyHID.on('HIDFunctionRequest', (operation) => {
switch (operation) {
case 'call_start':
if (allConditionsMet)
yourStartCallFunction();
else {
kandyHID.invokeHIDFunction('call_failure')
setTimeout(() => kandyHID.invokeHIDFunction('call_failure_finish', 1000));
}
break;
}
});
Ending an active call
An active call can be ended by:
- replacing the headset on the base
- pressing the MultiFunction button
- pressing the red Call End button on the base or the Call button for single-button devices
Muting / Unmuting
When on an active call, pressing the device's Mute button will cause 'call_mute' to be emitted. If the device is muted, 'call_unmute' will be emitted.
On the Evolve2 40, the call can also be muted by raising the microphone boom and unmuted by lowering it.
Hold / Resume
When the device is engaged in an active call, the call can be placed on hold or resumed by:
Jabra Engage 65
- pressing the green Call Answer button on the base
- pressing and holding the Multi-Function button on the headset for 1-2 seconds
Jabra Engage 50
- pressing and holding the Call Answer / End button on the Link controller for 1-2 seconds
Jabra Speak 750
- pressing the green Call Answer button on the base
Jabra Evolve2 40
- pressing and holding the Multi-Function button on the headset for 1-2 seconds
Jabra Engage 50 II
- pressing and holding the Multi-Function / Mute button on the Link controller for 1-2 seconds
Call Reject
Rejecting an incoming call can be accomplished by:
Jabra Engage 65
- pressing the red Call End button on the base
- double-clicking the Multi-Function button on the headset
Jabra Engage 50
- double-clicking the Call Answer / End button on the base
Jabra Speak 750
- pressing the red Call End button on the base
Jabra Evolve2 40
- double-clicking the Multi-Function button on the headset
Jabra Engage 50 II
- double-clicking the Call Start / End button on the Link controller
Call Swap
- When preconditions are met, performing a 'call_hold' action on the device (see above) will signal the controlling application to swap between the active and a held call. See call swap documentation.
Error Handling
Most errors - device connection, disconnection, power off, etc. - are handled gracefully and logged.
It's natural during app development that you may put the device into a state that is out of sync with your app. In that case, send it a 'reset' to return the device to default state.
Device Error
In VDI or Browser, if the active/selected device is powered off or disconnected from the Client device_error
will be emitted on the HIDFunctionRequest
event. In order to use the same or another device, it must be (re)selected.
Channel Error (legacy VDI only)
In legacy VDI eLux and virtual channel communication between the local SDK and the Driver running on the Thin Client is lost for any reason, channel_error
will be emitted on the HIDFunctionRequest
event. HID's channel will remain down and not automatically attempt to reconnect. Once your app chooses to reestablish communication, reissue initializeHIDDevices()
and then reselect the device for all device types.
Backwards Compatibility
When used in a Citrix eLux VDI environment, this SDK is backwards-compatible with the most recent and one (1) previous version of Kandy HID Driver for VDI (DLL) (official releases only). This is intended to allow Kandy HID Driver for VDI upgrades on the Thin Client installed-base to lag behind application updates.
See the compatibility matrix for Kandy HID Driver for VDI and Kandy HID SDK version compatibility information.
NOTE As of version 2.3.0, this SDK will use WebHID by default in non-legacy eLux VDI. If it's necessary to have your application use the Kandy HID Driver for VDI rather than WebHID, set the useDriver
flag in driverInfo
to true.
Example:
Make the following change to the sample code in Initializing the local instance for VDI
:
let driverInfo = ipcRenderer.sendSync('getDriverInfo')
// set 'useDriver' to true to force use of the HID Driver/DLL
driverInfo = {
...driverInfo,
useDriver: true
};
const kandyHID = initializeHIDDevices({ mode, driverInfo })
Electron Security
Use of Electron security features such as nodeIntegration, webSecurity and enableRemoteModule have no effect on this SDK's operation. This SDK includes a preload script you can include in your preload script for use with Electron's context isolation feature. See details here.
Known Issues / Limitations
- The same device must be selected as active microphone, speakers and alert speakers via
selectHIDDevice()
. - Going offhook on the Jabra Engage 65, Speak 750 or Evolve 2/40 may not cause 'call_start' to be sent up to the app due to non-deterministic behaviour of these devices in this scenario. Support tickets (272, 277) have been created with the device vendor.
- In VDI eLux when the Kandy HID Driver for VDI is used, the Jabra Speak 750 is known to conflict with either the mouse or keyboard when offhook. The issue has been addressed by the vendor in the RP6 / 64-bit version of the eLux OS image. There are no plans to address it in the RP5 / 32-bit version.
- See limitations relating to use of older versions of Kandy HID Driver for VDI in compatibility documentation.
- In Windows VDI, the LEDs on the Jabra Engage 50 may not be responsive if the device is not at factory default settings. If the Engage 50 LEDs are not changing state during call operations, first disconnect and reconnect the device. If the condition persists, reset the device using the latest available version of Jabra Direct. Note that kandy-hid always assumes devices are at factory default settings.
- In eLux VDI, it has been found that connecting a HID device to the Thin Client may cause eLux to select it as the system default device. As well, the previously selected device may be muted. Both of these state changes may affect in-progress and future calls until rectified. Resolution is to unmute and re-select devices in the eLux system menu. This is not related to the Kandy HID libraries but standard eLux behaviour.
- If a Jabra Engage 50 is using firmware version 2.3.1, it will not respond correctly to the 'stop_ringing' instruction; the headset will stop blinking, but the Link device will continue to blink indefinitely. The only known way to resolve the situation is to unplug and reconnect the device from the host PC. This issue affects all configurations when firmware 2.3.1 is used. Firmware version 1.24 works correctly and so is recommended in order to avoid this issue. The device vendor has indicated that this issue has been corrected in firmware version 2.10.0, now released.
- Issuing a call hold action from a HID device (e.g. a long-press on a single-buttoned device) may cause Apple Music on the client to start in a Mac environment. This is native MacOS behaviour and not related to this SDK.
- The Jabra Engage 50 II may be muted by the user while the call/device is on hold. However, when on hold, the device does not signal this state change to the SDK to interpret and forward to the controlling app. As a result, the device may end up in a muted state, out of sync with the parent app, once the call is resumed. Once in that state the device can be unmuted from the device.
- Use of Microsoft Teams or any other HID-capable application may interfere with this SDK's ability to reliably control HID devices. To ensure predictable behaviour and avoid undesirable side-effects, MS Teams should not be installed on the same PC where an application using this SDK is in use.
CHANGELOG
See CHANGELOG.