Managing calls
Implementing call control lets users manage calls in your application, directly from their Jabra device. For example, accepting calls, rejecting calls, putting a call on hold, or resuming a held call.
To manage calls, the Jabra library provides the following two modules, Call Control and Easy Call Control. The main difference between them is the level of abstraction.
The following description can help you decide the most optimal path to integration.
- Easy Call Control: Offers a higher-level solution and is intended to be quicker and easier to integrate. The high-level methods handle most of the complexities regarding control of the state of the device and interpreting signals that come from the device. This module enables you to integrate Single Call or Multiple Call functionalities. Choose Single call if you intend to only handle one call at a time.
However, if your application supports features such as handling multiple ongoing/active calls simultaneously, putting calls on hold, etc., you can choose Multiple Call functionalities. The approach of integrating with the Easy Call Control module with Single Call functionality partly removes the complexity of a manual integration. This is the recommended path.
- Call Control: This module provides a lower-level, more advanced API allowing greater flexibility and control. However, this path is more complex to integrate, and can be more prone to mistakes than Easy Call Control. The integrator must ensure that all operations are executed correctly.
The functionalities of both Single and Multiple Call are also available in the Call Control module; however, you must handle everything manually, including complications related to supporting Single or Multiple Call. For example, in Single or Multiple Call you must set your Jabra devices to offhook or onhook mode, ring the Jabra device, handle offhooking, as well as handling the flash signal from the device, among others.
Easy Call Control
To implement Easy Call Control, the following list are the suggested steps to follow. If you are already familiar with the topics suggested, you can proceed with initializing the module.
- Read about Handling connection changes, exceptions, and the initial state
- Read about Supported features that you would like to integrate
- Choose to integrate with Single Call or Multiple Call functionalities
- Initialize the Easy Call Control module
To use call control functionalities via the Easy Call Control module, you must
first initialize the module using the EasyCallControlFactory
class. This
class provides helper functions for taking control
over IDevice instances
and gives access to call control for the device.
The Easy Call Control module can be initialized with either one of the following: Single Call or Multiple Call functionalities. To initialize the module, see the following code samples.
Firstly, to initialize with Single Call functionality, proceed as follows.
import { EasyCallControlFactory, init, RequestedBrowserTransport } from '@gnaudio/jabra-js';
function wait(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function initialiseEccSingleCall(){
try {
const config = {
transport: RequestedBrowserTransport.CHROME_EXTENSION_WITH_WEB_HID_FALLBACK
};
// Initialize the Jabra SDK
const jabraSdk = await init(config);
// Set up for call control
const easyCallControlFactory = new EasyCallControlFactory(jabraSdk);
jabraSdk.deviceAdded.subscribe(async (device) => {
if (!easyCallControlFactory.supportsEasyCallControl(device)) {
return;
}
// Create Easy Call Control for single call handling
const singleCallControl = await easyCallControlFactory.createSingleCallControl(device);
// Call single-call-control related functionality, for example
// Start a call, wait five seconds, end the call.
await singleCallControl.startCall();
await wait(5000);
await singleCallControl.endCall();
});
} catch (err) {
console.log(err);
}
}
initialiseEccSingleCall();
To initialize with Multiple Call functionality, proceed as follows.
// Setup for IMultiCallControl...
import { EasyCallControlFactory, init, RequestedBrowserTransport } from '@gnaudio/jabra-js';
function wait(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function initialiseEccMultiCall(){
try {
const config = {
transport: RequestedBrowserTransport.CHROME_EXTENSION_WITH_WEB_HID_FALLBACK
};
// Initialize the Jabra SDK
const jabraSdk = await init(config);
// Set up for call control
const easyCallControlFactory = new EasyCallControlFactory(jabraSdk);
jabraSdk.deviceAdded.subscribe(async (device) => {
if (!easyCallControlFactory.supportsEasyCallControl(device)) {
return;
}
// Create Easy Call Control for multi call handling
const multiCallControl = await easyCallControlFactory.createMultiCallControl(device);
// Call multi-call-control related functionality, for example
// Start a call, wait five seconds, put call on hold, wait, resume, wait, then end the call.
await multiCallControl.startCall();
await wait(5000);
await multiCallControl.hold();
await wait(5000);
await multiCallControl.resume();
await wait(5000);
await multiCallControl.endCall();
});
} catch (err) {
console.log(err);
}
}
initialiseEccMultiCall();
Handling connection changes, exceptions, and the initial state
To help you avoid common pitfalls during integration, it is recommended that you read about handling changes, exceptions, and the initial state.
Handling connection changes
When setting up Easy Call Control, you must know how to handle disconnection cases. For example, when a user physically unplugs the device.
To listen for disconnect events, ensure you setup a subscription as follows:
multiCallControl.onDisconnect.subscribe(() => {
// this callback is triggered if the device disconnects
});
// or
singleCallControl.onDisconnect.subscribe(() => {
// this callback is triggered if the device disconnects
});
When a device is disconnected, the IMultiCallControl
or
ISingleCallControl
instances become invalid and can no longer be used.
To continue using Easy Call Control on a new device, you must create a new instance of
either IMultiCallControl
or ISingleCallControl
.
You must not call the teardown
command after disconnecting. In this case
it is handled automatically.
The Teardown method
You can use the teardown
method whenever you want to deactivate a
IMultiCallControl
or ISingleCallControl
instance.
The method removes internal event subscriptions to avoid memory leaks and, if taken, clear the internal call lock. The most common use case is when you are handling a change of device.
If you create a single-page web application or work on a similar project
with a persistent state, you must call teardown
whenever the
IMultiCallControl
or ISingleCallControl
instance is no longer needed.
For example, when changing views in the UI.
Failure to do so can cause undesirable behavior and memory leaks.
Initial state
If a device is disconnected -- or the user decides to change devices in the middle of a call, you must recover the device state when instantiating a new one.
You can set an initial state from the
EasyCallControlFactory.createMultiCallControl
and
EasyCallControlFactory.createSingleCallControl
methods, which take an
optional second argument containing the initial state. The following is
an example for IMultiCallControl
.
const initialState = {
ongoingCalls: 2,
muted: true,
onHold: false
}
const multiCallControl = await easyCallControlFactory.createMultiCallControl(
device,
initialState
);
// The device will have two ongoing calls and be muted at start up
And for ISingleCallControl
.
const initialState = {
callActive: true,
muted: true
}
const singleCallControl = await easyCallControlFactory.createSingleCallControl(
device,
initialState
);
// The device will be in a call and muted at start up
Handling a change of device
If the user wants to choose another device, you must first teardown
the
old IMultiCallControl
or ISingleCallControl
instance before creating a
new one.
The following is a pseudo-code example of how that can look like:
const yourAppState = {
ongoingCalls: 2,
muted: true,
onHold: false
}
// User selects new device in GUI
const newDevice = newDevice;
// Teardown old instance
// This will clear internal call locking and event subscriptions to avoid leaks
oldMultiCallControl.teardown();
// Set initial state for the new device
const initialState = {
ongoingCalls: yourAppState.ongoingCalls,
muted: yourAppState.muted,
onHold: yourAppState.onHold
}
// Create new instance
const newMultiCallControl = await easyCallControlFactory.createMultiCallControl(
newDevice,
initialState
);
// Now, you can continue your program using newMultiCallControl
Handling exceptions
In certain scenarios, the module can throw an exception. For example, if a Jabra device is being used by another application running on the computer (e.g., another application is using it for a call, then an exception is thrown if you try to use the device).
Most of the exceptions are not strictly avoidable and can happen due to the current state of the device.
For this reason, it is recommended that you have a strategy for handling
potential exceptions from every method in the ISingleCallControl
or
IMultiCallControl
class that has potential exceptions listed.
Supported features in Easy Call Control
Provided that an integration exists between the Jabra device and your application, end-users can manage and control a call with several features.
The following is not an exhaustive list of features. In addition to basic functions such as ringing a Jabra device, accepting/rejecting a call, end-users can also control a call using muting/unmuting and hold/resume functionalities.
Muting/Unmuting
You can unmute both from the device and from your application.
From the device:
When a user mutes the device by physically interacting with the Jabra device (e.g., muting by rotating the boom arm), some Jabra devices stop transmitting audio from the microphone and send a signal to the library.
The application then listens for these signals, acknowledges that it has received the signal, and stops sending audio to other attendees in the call.
From the application:
When muting through the application UI, the application must also request the device to mute through the Jabra library. Be aware that this is like a fire-and-forget API whereby the Jabra device does not respond to a mute request from the application.
Hold/Resume
The active call was either put on hold or the current active call was resumed.
Implementing Single or Multiple Call functionalities
After initializing the Easy Call Control module, you must implement either Single or Multiple Call functionalities.
If you are only to support a single call at a time, you must use Single Call. This is the preferred and easiest combination for an accelerated path to integration.
Be aware that you can start with Single Call functionalities and later extend to Multiple Call functionalities if your requirements are to change in the future.
On the other hand, if you have already decided to handle multiple calls and your application supports it, you can choose Multiple Call functionalities.
- Single Call: Refers to an API that enables a single call to be
accepted or rejected.
- Multiple Call: This allows the use of the
IMultiCallControl
interface to handle multiple calls simultaneously. This interface extends the Easy Call Control module to instruct the Jabra device that there are multiple calls on hold, start multiple calls simultaneously, and swap calls. Be aware that Multiple Call functionalities have a higher level of complexity than Single Call functionalities.
Also, be aware that in IMultiCallControl
, the callActive
observable is replaced by the ongoingCalls
observable.
Single and Multiple Call Functionalities
To make your integration easier, Single and Multiple Call functionalities, including code samples have been divided as follows:
- Handling new calls (Single and Multiple)
- Handling active or ongoing calls (Single and Multiple)
- Terminating a call (Single and Multiple)
- Other functionalities (Single and Multiple)
Handling new calls -- Single Call
Ringing the device
You can indicate to a device to start its ringing effect. This notifies the device that there is an incoming call. The ringing effect depends on the capabilities of the device, for example a flashing green LED light.
The following code sample shows how your application must react when receiving an incoming call and starts to ring the device:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
const wasAccepted = await singleCallControl.signalIncomingCall();
if (wasAccepted) {
// Call has started, proceed to update your UI
} else {
// Call was rejected, proceed to update your UI
}
signalIncomingCall
returns a promise that resolves
to true
or false
depending on whether the call was accepted or rejected;
the promise is resolved either from interacting with the (physical)
device buttons or by calling the
functions acceptIncomingCall
or rejectIncomingCall
.
Accepting or rejecting an incoming call
If your application has received a new incoming call and you have indicated that to the device (as mentioned in Start ringing the device), the device rings.
If the end-user interacts with your UI, you can either accept or reject
the call. The acceptance or rejection is done via the
acceptIncomingCall()
and rejectIncomingCall()
methods, respectively.
Similar to interacting with the device, executing either of these
methods resolves the promise returned by signalIncomingCall()
.
Note: The device will ring for 25000 milliseconds while waiting for a response. You can optionally provide your own timeout by passing an argument to the the method e.g. waiting for 35000 ms:
signalIncomingCall(35000)
.
- Accepting a call
The following code sample shows how to accept the call from your application:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
const incomingCall = singleCallControl.signalIncomingCall();
await singleCallControl.acceptIncomingCall(); // accept the call
const wasAccepted = await incomingCall; // wasAccepted will be true
- Rejecting a call
The following code sample shows how to reject the call from your application:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
const incomingCall = singleCallControl.signalIncomingCall();
singleCallControl.rejectIncomingCall(); // reject the call
const wasAccepted = await incomingCall; // wasAccepted will be false
Accepting or rejecting calls by interacting with the device
When the end-user interacts with the Jabra device and the device rings, the user can accept or reject the call by physically interacting with the device via a button press.
Be aware that this can be different depending on your device.
- Accepting a call
When the call is accepted (or the device is offhook), it resolves
the promise returned by signalIncomingCall()
. The promise is then
resolved with a true
value.
In both cases, regardless of whether the promise is resolved with a
true
or false
value, the device stops ringing.
See the following code sample:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
try {
const wasAccepted = await singleCallControl.signalIncomingCall();
// User presses start-call button on the device
if (wasAccepted) {
// Call was accepted, proceed to update your UI
}
else if (!wasAccepted) {
// Call was rejected, proceed to update your UI
}
} catch (err) {
// Something bad happened.
}
- Rejecting a call
Rejecting a call (or when the device is onhook) can be handled similarly to accepting a call. For example, the promise returned by signalIncomingCall() will be resolved. However, it resolves with a false value.
The following code sample shows how you can handle this scenario:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
const wasAccepted = await singleCallControl.signalIncomingCall();
// User presses end-call button on the device
if (!wasAccepted) {
// Call was rejected, proceed to update your UI
}
Starting a new call from your application
From the device's perspective, starting a new outgoing call and being in an active call (i.e., talking to someone) is handled in the same way -- you must indicate to the device that it is in an active call.
See the following code sample about the user interacting with your application and wanting to start a new call:
try {
const singleCallControl; // assume `ISingleCallControl` is already instantiated
await singleCallControl.startCall();
} catch (err) {
// Something bad happened. Inspect error.
// A common mistake is to call `startCall()` while the device is already in a call -- that would
// throw an exception for Single Call Control.
}
Once the call is answered, nothing should change on the device, as the
startcall()
function has already been called.
If the other party rejects the call, the call is considered ended. You can find more details and a sample scenario in Ending a call from your application.
Handling new calls -- Multiple Calls
Start Call command
The startCall()
command reacts differently from the Single Call
equivalent and lets you handle multiple calls in parallel.
When you start a call while another is already active, the current call gets put on hold, and the new call is activated.
Whenever a call is started, the ongoingCalls
observable emits a count
incremented by one.
See the following code sample:
try {
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall();
await multiCallControl.startCall();
multiCallControl.ongoingCalls.subscribe((ongoingCalls) => {
// ongoingCalls = 2
// because we started two calls.
});
} catch (err) {
// Something bad happened. Inspect error.
}
Accept an incoming call
The acceptIncomingCall
method generally works the same way as
the ISingleCallControl
equivalent, which has already been described.
The IMultiCallControl
interface allows you to specify what happens when
a call is already active, and you can either end the call (default) or
put the current call on hold.
For details on how to accept the incoming call and put the current call on hold, see the following code sample:
// End current call...
try {
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall(); // ongoingCalls = 1
multiCallControl.signalIncoming();
await multiCallControl.acceptIncomingCall(AcceptIncomingCallBehavior.END_CURRENT);
multiCallControl.ongoingCalls.subscribe((ongoingCalls) => {
// ongoingCalls = 1
// because we ended the current call and accepted the incoming call
});
} catch (err) {
// Something bad happened. Inspect error.
}
// Put current call on hold...
try {
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall(); // ongoingCalls = 1
multiCallControl.signalIncoming();
await multiCallControl.acceptIncomingCall(AcceptIncomingCallBehavior.HOLD_CURRENT);
multiCallControl.ongoingCalls.subscribe((ongoingCalls) => {
// ongoingCalls = 2
// because we put the current call on hold and accepted the incoming call
});
} catch (err) {
// Something bad happened. Inspect error.
}
Handling active or ongoing calls -- Single and Multiple Calls
Changing and keeping track of the mute state of the device
During the lifetime of a call, the user can mute or unmute their microphone many times. Depending on the design of your application, the user could mute / unmute the microphone by pressing a button on the Jabra device.
When the user does so, it is important that you also indicate to the Jabra device to do this.
Most Jabra devices can mute by interacting with the device itself (e.g., physically manipulating the boom arm). This interaction is described in more detail in Keeping track of the mute state.
Interacting with your application requires executing mute()
or unmute()
.
Moreover, to keep your application's state synchronized with the device
state, ensure you keep track of what the device is doing or when any
changes occur.
The following code sample shows how to subscribe to changes in the mute state, as well as how to mute and unmute:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
await singleCallControl.startCall(); // Cannot mute/unmute if not in a call
singleCallControl.mute(); // indicates that the microphone should mute
singleCallControl.unmute(); // indicates that the microphone should not be muted anymore
singleCallControl.muteState.subscribe((muteState) => {
// muteState = no-ongoing-calls
// muteState = 'unmuted' (will unmute by default when starting a call)
// muteState = 'muted'
// muteState = 'unmuted'
});
Handling active or ongoing calls -- Multiple Calls
Put a call on hold or resume a call
The IMultiCallControl
interface lets you put an active call on hold with
the hold()
command and resume it with the resume()
command.
These commands are only available when the device is in an active call; otherwise, an exception is thrown. See the following sample code, showing how to put a call on hold or resume a call:
try {
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall(); // hold only works if a call is started
await multiCallControl.hold();
multiCallControl.holdState.subscribe((holdState) => {
// holdState = 'on-hold'
});
} catch (err) {
// Something bad happened. Inspect error.
}
In the following code sample, you can see a full sequence, where the
holdState
changes when calling the different commands in the sequence.
try {
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall(); // holdState = 'not-on-hold'
await multiCallControl.hold(); // holdState = 'on-hold'
await multiCallControl.resume(); // holdState = 'not-on-hold'
await multiCallControl.endCall(); // holdState = 'no-ongoing-calls'
} catch (err) {
// Something bad happened. Inspect error.
}
Swap call requests
A swap call request means that the user wants to swap from the current (active) call to the next call on hold.
When starting two or more calls, the device can emit swap call requests. On your device, it is triggered by the same button that activates the Hold functionality when one call is ongoing.
See the following sample code for two ongoing calls.
const multiCallControl; // assume `IMultiCallControl` is already instantiated
// Swap works with a minimum of two ongoing calls
await multiCallControl.startCall();
await multiCallControl.startCall();
// User long-presses the multi-function-button on the device
multiCallControl.swapRequest.subscribe(() => {
// The observable callback is called
// On the softphone side, put the active call on hold and resume the previously held call
});
Important note
Easy Call Control provides limited handling of swap call events as the desired behavior varies between application implementations. Consider the following:
See the following code sample on how to resume the last current call.
// Example of how to resume the "last current call"
const multiCallControl; // assume `IMultiCallControl` is already instantiated
await multiCallControl.startCall(); // first call started
await multiCallControl.startCall(); // second call started, it is now the current call while firstCall is on hold.
await multiCallControl.hold(); // hold current call, meaning that both calls are on hold
// Subscribe to changes in holdState so we know the current state
let currentHoldState;
multiCallControl.holdState.Subscribe((newHoldState) => {
currentHoldState = newHoldState;
});
// User long-presses the multi-function-button on the device
multiCallControl.swapRequest.subscribe(async () => {
// Assess the current holdState
if (currentHoldState === "on-hold") {
// `resume()` will put the device back into an active call state,
// however, you need to handle on the softphone side which of the held calls
// to resume
await multiCallControl.resume();
}
});
Ongoing calls
As previously explained in Start call command, you can subscribe to
the ongoingCalls
counter.
See the following code sample:
const multiCallControl; // assume `IMultiCallControl` is already instantiated
multiCallControl.ongoingCalls.subscribe((ongoingCalls) => {
// ongoingCalls equals to the number of calls started
});
Whenever you start a call -- either
through startCall()
or acceptIncomingCall()
-- this count increments by
one. It does not matter if the ongoing/active calls are on hold or not
-- it is the total sum of calls.
At the same time, whenever a call is ended by calling endCall
or by
pressing the End call button on the device, the count decrements by
one.
When the count gets back to zero, the device ends the active call state and returns to an idle or inactive state. You can read more about ending a call in Terminating a call.
Terminating a call -- Single and Multiple Calls
Ending a call from your application
During an active call, the user may want to end the call. Your application may offer this functionality to the user, for example a red handset button in the UI, indicating the ability to hang up or end the call.
When the user presses the button, the call ends on your application. However, the Jabra device remains in an active call state until you indicate to it that the call has ended.
The following code sample shows how to end the call (or onhook):
try {
const singleCallControl; // assume `ISingleCallControl` is already instantiated
await singleCallControl.endCall();
} catch (err) {
// Something bad happened. Inspect error.
}
Ending a call by interacting with the device
Jabra devices let you end a call from the device itself. Therefore, your application must stop the call when the device indicates that the user interacted with it and wants to hang up or end the call.
You can do this by subscribing to the call state and acting appropriately when transitions/state changes occur from in a call (active) to not in a call state.
Other functionalities
Change the device used in an active call -- Multiple Calls
The Easy Call Control module uses a device-based abstraction. Therefore, changing the device requires creating a new instance, and passing a reference to the new device.
Additionally, you must also indicate to the old device that it is no
longer used in that call. To do so, ensure you execute the teardown
method.
To stop receiving updates for that device, you must also unsubscribe all observables. See the following code sample to change the device used in an active call.
const oldDeviceCallControl; // assume `ISingleCallControl` or `IMultiCallControl` is already instantiated
const newDeviceCallControl; // assume `ISingleCallControl` or `IMultiCallControl` is already instantiated
// For IMultiCallControl you should follow the same pattern for ongoingCalls, holdState and swapRequest
let muteStateSub = oldDeviceCallControl.muteState.subscribe(); // assume you have stored Subscriptions for muteState
let callActiveSub = oldDeviceCallControl.callActive.subscribe(); // assume you have stored Subscriptions for callActive
// End the call on the old device
await oldDeviceCallControl.endCall();
// Unsubscribe
muteStateSub.unsubscribe();
callActiveSub.unsubscribe();
// Teardown old instance
oldDeviceCallControl.teardown();
// Subscribe to state changes for the new device
muteStateSub = newDeviceCallControl.muteState.subscribe();
callActiveSub = newDeviceCallControl.callState.subscribe();
try {
await newDeviceCallControl.startCall();
} catch (err) {
// Something bad happened. Inspect error.
}
Keeping track of the Active Call state of the device -- Single Call
In addition to keeping track of the mute state, it is also important
to keep track of the callActive
or Active Call state. If you do not do
so, it can mean that your call ends, but your application will not
reflect that the call has ended.
When a user accepts or starts a call from your application, the application must execute commands and reflect the changes in the state of the device.
Any call-related actions such as accepting an incoming call, starting,
or ending a call, and others, trigger the callActive
observable to emit
a change in call state.
The following code sample shows how to subscribe to changes in the call state:
const singleCallControl; // assume `ISingleCallControl` is already instantiated
singleCallControl.callActive.subscribe((callActive) => {
if (callActive) {
// the device is in an "active call state", reflect on the changes (e.g. update the UI)
} else {
// the device is no longer in a call state, reflect on the changes (e.g. update the UI)
}
});
Call Control (CC)
The CC module is the most advanced of the integration techniques, with a more advanced API and is more complex to integrate than the Easy Call Control module.
When implementing this module, ensure that your Jabra device supports call control before creating a call control instance. You must also acquire a call lock, handle exceptions, connection changes, and others.
The following code sample does the following:
Initializes the Call Control module
Checks that your Jabra device supports call control (i.e., whether it can perform call control functionalities)
Creates an instance of
ICallControl
, letting you perform call control with the specified Jabra device
import { CallControlFactory, init, RequestedBrowserTransport } from '@gnaudio/jabra-js';
function wait(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
// This function uses the depreceated terminology "acc". But it does refer to Call Control (CC).
async function initialiseAcc(){
try {
const config = {
transport: RequestedBrowserTransport.CHROME_EXTENSION_WITH_WEB_HID_FALLBACK
};
// Initialize the Jabra SDK
const jabraSdk = await init(config);
// Set up for call control
const callControlFactory = new CallControlFactory(jabraSdk);
jabraSdk.deviceAdded.subscribe(async (device) => {
if (!callControlFactory.supportsCallControl(device)) {
return;
}
const deviceCallControl = await callControlFactory.createCallControl(device);
console.log('Device Call Control ready.');
// Try example operations on the call control device
// To do this we must take the call lock
// We release the call lock when we are done.
const gotLock = await deviceCallControl.takeCallLock();
if (gotLock) {
deviceCallControl.offHook(true);
console.log('Device Off Hook.');
await wait(3000);
deviceCallControl.offHook(false);
console.log('Device Back On Hook.');
deviceCallControl.releaseCallLock();
}
});
} catch (err) {
console.log(err);
}
}
initialiseAcc();
You can initialize the module via the CallControlFactory class. Once the
module is initialized, you can create an
ICallControl
object by passing an Idevice
instance.
Acquiring a call lock
After you initialize the library and discover devices, the following code sample shows how to acquire a call lock.
const deviceCallControl = await callControlFactory.createCallControl(device);
try {
const gotCallLock = await deviceCallControl.takeCallLock();
if (gotCallLock) {
// You have the call lock and can now set device states
// ...
}
} catch (err) {
// An exception will be thrown if you already have the call lock
console.log(err);
}
Releasing a call lock
The lock remains active for the duration of the call. When the call finishes, the lock must be released, which lets other applications use the device.
After acquiring a call lock, it can be released as shown.
try {
deviceCallControl.releaseCallLock();
} catch (err) {
// An exception will be thrown if you did not have the call lock
console.log(err);
}
Call lock code sample
Using call control, see the following code sample showing how to:
- Acquire and release a call lock
- Ring a device
- Stop the ringer after two seconds
import { CallControlFactory, IDevice, init, RequestedBrowserTransport } from '@gnaudio/jabra-js';
import { filter, first } from 'rxjs/operators';
function wait(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function ringDevice(callControlFactory: CallControlFactory, device: IDevice) {
// Initialize call control
const deviceCallControl = await callControlFactory.createCallControl(device);
console.log("Call Control ready for device.");
// Acquire call lock
let gotCallLock = await deviceCallControl.takeCallLock();
if (gotCallLock) {
console.log("Call Lock obtained.");
// Ring the device, wait and then stop ringing. Assume user did not answer.
deviceCallControl.ring(true);
console.log("Ringing device.");
await wait(2000);
deviceCallControl.ring(false);
console.log("Stop ringing device.");
// Release the call lock. In general care should be taken to always release this,
// even if an error occurs.
deviceCallControl.releaseCallLock();
console.log("Call Lock released.");
}
}
async function demo() {
const config = {
transport: RequestedBrowserTransport.CHROME_EXTENSION_WITH_WEB_HID_FALLBACK
};
try {
const jabraSdk = await init(config);
const callControlFactory = new CallControlFactory(jabraSdk);
// Subscribe to changes to the device list and take the first device that we find
// for our example, as long as it supports call control.
jabraSdk.deviceList.pipe(
filter((devices) => devices.some(d => callControlFactory.supportsCallControl(d))),
first()
).subscribe((devices) => {
const firstDevice = devices.find((d) => callControlFactory.supportsCallControl(d));
if (firstDevice) {
console.log(`Using: ${firstDevice.name} - ${firstDevice.productId}`);
// Ring the device
ringDevice(callControlFactory, firstDevice);
} else {
console.log("Unable to find any devices which support call control.");
}
});
} catch (err) {
console.log(err);
}
}
demo();
Handling connection changes and exceptions
Handling connection changes
Physical devices can have multiple connections. For example, a Jabra Evolve2 85 headset might be connected both via USB and through a Bluetooth Jabra Link 380 dongle, with both connections having call control capabilities.
When the ICallControl
instance is created, it uses exactly one of the
device's connections. If that connection is removed, but the device has
another connection capable of call control, you must create a
replacement ICallControl
instance.
Additionally, the device's internal state resets, so if your application was currently manipulating the device in any way, you will have to restore that state manually. This includes taking the call lock as the mandatory prerequisite step.
Moreover, the ICallControl
interface exposes an onDisconnect
observable,
which emits whenever the connection used by the ICallControl
instance
disconnects.
The following sample code shows the basic steps in handling
onDisconnect
.
import { CallControlFactory, ICallControl, IDevice, init, RequestedBrowserTransport } from '@gnaudio/jabra-js';
async function accConnectionChanges() {
const config = {
transport: RequestedBrowserTransport.CHROME_EXTENSION_WITH_WEB_HID_FALLBACK
};
try {
const jabraSdk = await init(config);
// Set up a CallControlFactory
const callControlFactory = new CallControlFactory(jabraSdk);
// When a device is added, set things up so we update things correctly
// when a connection to that device is removed.
jabraSdk.deviceAdded.subscribe(async (device) => {
if (callControlFactory.supportsCallControl(device)) {
let deviceCallControl = await callControlFactory.createCallControl(device);
console.log(`Device connected: ${device.name}`);
deviceCallControl.onDisconnect.subscribe(async () => {
// Whenever the connection disconnects, see if there is another connection which
// is capable of call control.
console.log("Disconnecting");
if (!callControlFactory.supportsCallControl(device)) {
console.log("No connection");
return;
}
// There is an available connection, create a new ICallControl
deviceCallControl = await callControlFactory.createCallControl(device);
console.log('Connection remains - creating a new call control interface.');
// Restore the state of the device.
});
}
});
} catch (err) {
console.log(err);
}
}
accConnectionChanges();
Handling exceptions
In certain scenarios, the CC module can throw an exception. For example, if a Jabra device is being used by another application (an application is using it for a call) an exception is thrown if you try to use the device.
Most of these exceptions are not strictly avoidable and can happen due to the device's current state.
For this reason, it is recommended that you have a strategy for handling
potential exceptions from every method in the ICallControl
class.
Supported features in the Call Control module
Provided that an integration exists between the Jabra device and your application, an end-user can manage and control a call with the following supported features:
Setting the ringer ON/OFF
Notifies that there is an incoming call. This is an important signal as it is used to notify of an incoming call and can have different effects in different scenarios.
For example, a device might respond with a flashing green LED whilst your application might play its own ringtone.
Change call status
Notifies the device that a call was either started or ended. The behavior of a device depends on the call status, for example, the device cannot be muted unless it is in an active call.
Additional effects depend on the capabilities of your Jabra devices, for example, setting a solid red LED light on your headset during a call.
Mute/Unmute
For information on how to mute/unmute from the device and application, forcing a mute state, diagrams, reacting to device signals, common issues, and others, refer to Call Control documentation.
Hold/Resume
Notifies the device that the call state changed, but that the call has not ended yet. This means that the call was either put on hold or was resumed. The effect depends on the firmware of the device.