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 ICallControlobject 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 ICallControlclass.

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.