Manage Call

By implementing Call Control, it will be possible for the user to manage a call in your app from their device.
They will be able to:

  • Accept an incoming call.
  • Reject an incoming call.
  • End an active call.

Some example scenarios are given in the accompanying Manage Call section of the Integrator's Guide. The Jabra SDK provides two modules that can be used to implement call control functionality on your softphone. The CallControl module provides a lower-level, more advanced API which allows greater flexibility and control. The EasyCallControl module offers a higher-level solution and is intended to be quicker and easier to integrate. Both of these modules are described further on this page.

More details

In the previous section we introduced some of the basic scenarios covered by Call Control. There are, in fact many possible points where integration between a headset and softphone software might be required. For example: - Reflecting the state of a call (active call, hold, inactive). - Reflecting the microphone mute state (muted or unmuted). - Reacting to signals coming from a device (e.g. button presses): - Accepting or rejecting calls. - Putting a call on hold or resuming a held call. - Muting or unmuting the microphone. - Redial or dial. - Note: It is important that signals originating from the device are acknowledged. See the page on Mute and Other Signals for more details. - Setting the ringer on or off (indicated by either audio or blinking LEDs on the device).

Try Demo

The CallControl Module

The following sample code shows some of the basic steps involved in implementing Call Control via the CallControl module.

import { init, CallControlFactory } from "@gnaudio/jabra-js";
  try {
    const api = await init();
    const callControlFactory = new CallControlFactory(api);
    api.deviceAdded.subscribe(async (device) => {
      if (!callControlFactory.supportsCallControl(device)) {
        return;
      }
      const deviceCallControl = await callControlFactory.createCallControl(
        device
      );
      console.log('deviceCallControl ready');

      // Try an example operation on the call control device.
      deviceCallControl.offHook(true);
    });
  } catch (err) {
    console.log(err.message);
  }

Initializing the Call Control module

In the above code you can see that the first step is to initialize the module. This is done via the CallControlFactory class. The class provides helper functions for taking IDevice instances and giving you access to their call control functionality. For more on module initialization see the relevant section of the Initialize page of this Guide.

Once the module is initialized, you can take any IDevice instance to create an ICallControl object, which you can then use for any call control functionality (for that device).

Whenever we talk about a devices' call control functionality, we will assume you have created an ICallControl instance for that device.


Call lock

The first step in being able to change any of the devices' states is acquiring a call lock. For more on this, see the Call Lock page of this Guide.

Supported features

In the following sections we will run through the general behaviour of devices with respect to various features related to call control.

Set Ringer

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 a softphone might play its own ringtone. The end-user also cannot reject a call unless the device is told to ring.

[Note] The ring effect might differ depending on whether the device is in an active call.

sequenceDiagram
participant a as App
participant b as Jabra library
participant c as Jabra device
Note over a: [Prerequisite] Ensure call lock
opt Turn on ringer
a ->> b: device.ring(true)
b ->> c: #20
Note over c: ring effect starts
end
opt Turn off ringer
a ->> b: device.ring(false)
b ->> c: #20
Note over c: ring effect stops
end

Change call status (i.e. offHook)

Notifies the device that a call was either started or ended. The behaviour of a device depends on the call status, for example the device cannot be muted unless it is in a call. Additional side-effects depend on the capabilities of the device, for example setting a solid red LED while in a call.

[Note] Successfully changing the status of a call will result in an acknowledgement signal being sent by the device. Refer to the section Reacting to HOOK_SWITCH of the Mute and Other Signals page of this Guide for more information.

[Note] The term offHook comes from the HID standard and relates to old phones that had a physical hook that was released whenever the handset was picked up by the user.

sequenceDiagram
participant a as App
participant b as Jabra library
participant c as Jabra device
Note over a: [Prerequisite] Ensure call lock
opt On call start
a ->> b: device.offHook(true)
b ->> c: #20
alt Device successfully changes state to 'in a call'
Note over c: in-call effect starts
c ->> b: HOOK_SWITCH signal
b ->> a: device.deviceSignals emits
else Device does not change state
Note over a,c: No acknowledgement signal sent by device
end
end
opt On call end
a ->> b: device.offHook(false)
b ->> c: #20
alt Device successfully changes state to 'not in a call'
Note over c: in-call effect stops
c ->> b: HOOK_SWITCH signal
b ->> a: device.deviceSignals emits
else Device does not change state
Note over a,c: No acknowledgement signal sent by device
end
end

Mute/Unmute

For more on this see the Mute page of the Integrator's Guide and the Mute and Other Signals page of this Guide.

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. Effect depends on the firmware of the device.

[Note] Putting a call on hold is usually accompanied by starting a new call. Handling multiple calls is an advanced topic and is not covered here.

sequenceDiagram
participant a as App
participant b as Jabra library
participant c as Jabra device
Note over a: [Prerequisite] Ensure call lock
opt Hold call
a ->> b: device.hold(true)
b ->> c: #20
Note over c: on-hold effect starts
end
opt Resume call
a ->> b: device.hold(false)
b ->> c: #20
Note over c: in-call effect starts
end

Reacting to device signals

This is an important topic. See the Mute and Other Signals page of this Guide for more details.

Handling connection changes

As discussed in the Manage devices part of the Developer's guide, physical devices can have multiple connections. As an example, an Evolve2 85 headset might be connected both via USB and a Jabra Link 380 dongle, with both of these connections having call control capabilities.

When the ICallControl instance is created, it will use exactly one of the device's connections. If that connection gets removed, but the device has another connection capable of call control, it is your responsibility to create a replacement ICallControl instance. Additionally, the device's internal state will reset, so if your software solution 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.

The ICallControl interface exposes a onDisconnect Observable, which emits whenever the connection used by the ICallControl instance disconnects.

In contrast, the EasyCallControl module attempts to offer a best effort solution, which handles connection changes and restores the state of the device when those changes happen. This is explained in the Handling connection changes section of this part of the guide.

The following sample code shows the basic steps in handling the onDisconnect:

// Set up a `ICallControl` object
const api = await init();
const callControlFactory = new CallControlFactory(api);
let deviceCallControl = await callControlFactory.createCallControl(
  device
);

deviceCallControl.onDisconnect.subscribe(() => {
  // Whenever the connection disconnects, see if there's another call-control-capable connection
  if (!callControlFactory.supportsCallControl(device)) {
    return;
  }

  // There is an available connection, create a new `ICallControl`
  deviceCallControl = await callControlFactory.createCallControl(
    device
  );

  // Restore the state of the device
});

The EasyCallControl Module

Overview

The Easy Call Control module covers the same integration scenarios as the Call Control module.

The main difference in the two modules comes in the level of abstraction:

  • The Easy Call Control module offers high-level methods that handle most of the heavy lifting in regards to state control and interpreting signals that come from the device.
  • The regular Call Control module offers lower-level methods and it is up to you to make sure all operations are executed correctly.

Prerequisites

Initializing the easy call control module

In order to utilize the call control functionality, you first need to initialize the module.

Initialization is performed via the EasyCallControlFactory class. This class provides helper functions for taking IDevice instances and giving you access to their call control functionality. You can also read a detailed guide about module initialization in the Developer's Guide page on Initialization.

The Easy Call Control module can be initialized with one of two sets of functionality: single call handling or multiple call handling. The former is simpler to setup and use, so if you do not need to handle multiple ongoing calls and putting calls on hold, we advise you to use single call handling.

[Note] You should not use single call handling and multiple call handling at the same time as the modules won't synchronize state between each other.

// Setup for ISingleCallControl...
const easyCallControlFactory = new EasyCallControlFactory(coreApi);

// Whenever the core module detects new devices...
coreApi.deviceAdded.subscribe(async (newCoreDevice) => {
  // Easy Call Control for single call handling
  const singleCallControl =
    await easyCallControlFactory.createSingleCallControl(newCoreDevice);

  // Call single-call-control related functionality, for example
  singleCallControl.startCall();
  singleCallControl.endCall();
  // etc.
});
// Setup for IMultiCallControl...
const easyCallControlFactory = new EasyCallControlFactory(coreApi);

// Whenever the core module detects new devices...
coreApi.deviceAdded.subscribe(async (newCoreDevice) => {
  // Easy Call Control for multiple calls handling (note different method compared to previous example)
  const multiCallControl = await easyCallControlFactory.createMultiCallControl(
    newCoreDevice
  );

  // Call multi-call-control related functionality, for example
  multiCallControl.startCall();
  multiCallControl.hold();
  multiCallControl.resume();
  multiCallControl.endCall();
  // etc.
});

For the rest of this tutorial, whenever we talk about a devices' call control functionality, we will assume you have created an IMultiCallControl or ISingleCallControl instance for that device.

Handling connection changes

Unlike the CallControl module, the EasyCallControl module will attempt to handle all connection changes for you. This includes restoring the state of the device, which resets when a physical connection to the device gets removed.

Still, there is the possibility that the device remains connected, but none of the remaining connections have any call control capabilities. When this happens, an onDisconnect Observable exposed on the IEasyCallControl object will emit. If there are no remaining capable connections, the device should no longer be used for call control, and your software product should update its GUI to represent that.

Exceptions

In certain scenarios, the module might throw an exception. For example, if a device is being used by another software product running on the machine (e.g. another softphone is using it for a call, an exception will be thrown if you try to use the device.

Most of these exceptions are not strictly avoidable and happen due to the current state of the device.

For this reason, we recommend you have a strategy for handling potential exceptions from every method in the ISingleCallControl or IMultiCallControl class that has potential exceptions listed.

SingleCallControl

Try Demo

In this section we will go through the APIs related to single call handling. The MultiCallControl interface will be explained in the following section, but please read this one first as the basic functionality will be the same.

Start ringing the device

You can instruct a device to start its ringing effect. This notifies the device that there is an incoming call. The effect depends on the capabilities of the device, usually a flashing green LED.

Use this scenario when your softphone receives a new incoming call.

To start ringing, use the following snippet:

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 resolving to true or false depending on whether the call was accepted or rejected from either device buttons or by calling acceptIncomingCall or rejectIncomingCall.

[Note] the signalIncomingCall promise will pause your program until resolved by accept/reject. If you want to proceed immediately, use the then-syntax or move the await to where you want to resolve the promise.

Accepting incoming calls via device interaction

When the user interacts with the device and the device is ringing, they can indicate they wish to accept the call. This is usually a button press but can differ based on the product.

This user interaction will start the call automatically and will resolve the promise returned by signalIncomingCall(). The promise will be resolved with a true value.

It is important to reflect on this and update the state of your softphone in order to keep it synchronized to the state of the device.

[Note] Accepting a call via device interaction will also emit a change on the callActive observable. This is described in more detail in the section on keeping track of the callActive state of the device, below.

The following snippet shows the place where this scenario should be handled:

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
  }
} catch (err) {
  // Something bad happened.
}

If the incoming call is accepted while another call is active, the current call will be ended and the new call started.

Rejecting incoming calls via device interaction

Rather than accepting a call (described above), the user can decide to indicate they wish to reject a call.

This is handled in a similar way - the promise returned by signalIncomingCall() will be resolved, however, it will resolve with a false value.

It is important to reflect on this and update the state of your softphone in order to keep it synchronized to the state of the device.

The following snippet shows the place where this scenario should be handled:

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
}

Accepting and rejecting incoming calls from your softphone

If your softphone has received a new incoming call and you've indicated that to the device (via the start ringing scenario, described in a previous section), the device will start ringing. Depending on the design of your softphone, this incoming call can probably be accepted or rejected by interacting with your GUI (e.g. pressing the green handset icon, indicating the desire to pick up).

If the user interacts with your GUI, you should either accept or reject the call, then inform the device of this new state.

This is done via the acceptIncomingCall() and rejectIncomingCall() methods. Similar to interacting with the device, executing either of these methods will resolve the promise returned by signalIncomingCall().

The following snippet shows how to accept the call from your softphone:

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

The following snippet shows how to reject the call from your softphone:

const singleCallControl; // assume `ISingleCallControl` is already instantiated

const incomingCall = singleCallControl.signalIncomingCall();

singleCallControl.rejectIncomingCall(); // reject the call

const wasAccepted = await incomingCall; // wasAccepted will be false

Starting a new call from your softphone

From the device's perspective, starting a new outgoing call (i.e. calling someone and waiting for them to pick up) and being in an active call (i.e. talking to someone) is handled in the same way - you need to indicate that the device is in a call.

Therefore, you should use this scenario when the user interacts with your softphone and desires to start a new call. Once the call is picked up from the other party, nothing should change on the device. If the other party rejects the call, the call is considered ended and you should use the ending a call scenario, described above.

The following snippet shows how 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.
}

[Note] the EasyCallControl module does not support starting a new call directly from the device.

Changing the mute state of the device

Throughout the lifetime of a call, the user might mute or unmute their microphone multiple times.

Depending on the design of your softphone, they might be able to mute or unmute the microphone by interacting with your GUI. When they do so, it is important that you indicate this to the device.

[Note] Most devices offer a way to mute by interacting with the device itself (e.g. physically manipulating the boom arm). This interaction is described in more detail in the keeping track of the mute state section, later.

The following snippet shows 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

[Note] Executing unmute() while the boom arm is raised will not unmute the microphone. In practice, the mute state will briefly change to "unmuted" before changing back to "muted".

Ending a call from your softphone

While in an active call, the user might wish to end the call. Your softphone probably offers this functionality to the user (e.g. a red handset icon indicating the ability to hang up). When the user presses your GUI button, the call ends on your softphone. However, the device will remain in "active call" state until you instruct it that the call ended.

Alternatively, the call can end because the other party hung up and you still need to instruct the device that the call ended.

The following snippet shows how to end the call:

try {
  const singleCallControl; // assume `ISingleCallControl` is already instantiated

  await singleCallControl.endCall();
} catch (err) {
  // Something bad happened. Inspect error.
}

Ending a call via device interaction

As Jabra devices provide the ability to hang up from the device itself, your softphone should be able to stop the call when the device indicates that the user interacted with it and wishes to hang up.

This scenario is handled by keeping track of the call state and paying attention to transitions from "in a call" to "not in a call" states. It is important to reflect on this and update the state of your softphone in order to keep it synchronized to the state of the device. This is described in more detail in the keeping track of the call state section, later.

Keeping track of the mute state of the device

Throughout the lifetime of a call, the user might mute or unmute their microphone multiple times. They might do so by interacting with your softphone or by manipulating the device itself (e.g. raising the boom arm to mute the microphone).

While the first case requires action from your softphone (executing mute() or unmute()), interacting with the device requires you to reflect on any changes in order to keep the softphone's state synced to the device state.

[Note] Both mute/unmute and physical interaction with the device will lead to muteState emitting the new mute state.

The following code snippet shows how to subscribe to changes in the mute state:

const singleCallControl; // assume `ISingleCallControl` is already instantiated

singleCallControl.muteState.subscribe((newMuteState) => {
  if (newMuteState === "muted") {
    // the microphone was muted, reflect on the changes (e.g. update the UI)
  } else if (newMuteState === "unmuted") {
    // the microphone was unmuted, reflect on the changes (e.g. update the UI)
  } else if (newMuteState === "no-ongoing-calls") {
    // the device can only mute/unmute when in a call
  }
});

[Tip] use the MuteState enum instead of strings, this can be imported from the jabra-js package.

Keeping track of the callActive state of the device

Similar to keeping track of the mute state, it is important to keep track of the callActive state. Since the user can accept (or start) a call from both your softphone and from interacting with the device, your solution needs to both be able to execute commands, but also reflect on state changes.

Any call-related actions (accepting an incoming call, starting a call, ending a call, etc.) will trigger the callActive observable to emit.

The following code snippet 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)
  }
});

MultiCallControl

Try Demo

This section will describe how to use the IMultiCallControl interface for handling multiple calls scenarios.

This interface extends the Easy Call Control functionality with the ability to put the device on hold, start multiple calls at the same time and swap between the calls.

It is not straightforward to create a softphone that handles multiple calls, but it is important that your softphone GUI can handle the different states and commands this module provides.

[Tip] You will get the best development experience for implementing multiple calls handling if you use a Jabra device that displays multiple calls such as the Jabra Engage 75 with base. Other Jabra devices will work as well, but you will not get the same visual feedback.

Topics already covered

IMultiCallControl is similar to ISingleCallControl which was explained in the previous section (under the hood it runs on the same code), so in this section, we will not go into details on the subjects already covered.

The following APIs are exactly the same:

  • muteState
  • endCall()
  • signalIncomingCall()
  • rejectIncomingCall()
  • mute()
  • unmute()

Please see the single call control section, above, on how to work with these.

[Note] callActive is replaced by ongoingCalls in IMultiCallControl.

Start call

The startCall() command reacts a bit differently from the single call control equivalent. Here, it will allow you to start multiple calls in parallel.

When starting a call, while another call is active, the current call will go on hold and the new call will be activated.

Whenever a call is started, the ongoingCalls observable will emit a count incremented by one.

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.
}

It is important to keep the ongoingCalls count in sync with your softphone's list of ongoing calls.

More details on startCall can be found in the ISingleCallControl section onm starting a new call from your softphone, above.

Accept incoming call

acceptIncomingCall generally works the same as the ISingleCallControl equivalent, which has been described previously. However, the IMultiCallControl interface allows you to specify what should happen when a call is already active. You can either:

  • End current call (default)
  • Put current call on hold
// End current call...

try {
  const multiCallControl; // assume `IMultiCallControl` is already instantiated

  await multiCallControl.startCall(); // ongoingCalls = 1

  multiCallControl.signalIncoming();
  await multiCallControl.acceptIncomingCall("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("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.
}

[Tip] use the AcceptIncomingCallBehavior enum instead of strings, this can be imported from the jabra-js package.

Hold and resume

The IMultiCallControl interface allows you to put the current call on hold with hold() and resume it afterwards with resume().

These commands are only available when the device is in an active call, and will throw an exception otherwise

[Note] These commands only apply to the currently active call. They will not affect any secondary held calls if you have multiple calls ongoing.

Whenever the hold() and resume() commands are called, the holdState observable will emit accordingly. The holdState only applies to the state of the currently active call and not any secondary held calls.

To activate a secondary held call, please see the next section which describes Swap Requests.

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.
}

And here's a snippet showing a full 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.
}

[Tip] use the HoldState enum instead of strings, this can be imported from the jabra-js package.

With one ongoing call, you can trigger hold/resume from the device by pressing the flash-button. This will trigger the holdState observable to emit the updated state - you should synchronize your softphone state accordingly. With more than one ongoing call, the flash-button will emit a swapRequest (see the next section for more details).

The position of the flash-button that triggers hold varies from device to device; on some devices it is triggered by long-pressing the multi-function button, on other devices it is triggered by pressing the start-call button.

Swap request

When starting two or more calls, the device can emit swap-requests. This is triggered by the flash-button, which is the same button that triggers hold when one call is ongoing.

Swap request means that the user wishes to swap from the current active call to the next call on hold.

const multiCallControl; // assume `IMultiCallControl` is already instantiated

// Swap works with a minimum of two ongoing calls
await multiCallControl.startCall();
await multiCallControl.startCall();

// User presses flash-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
});

Easy Call Control provides limited handling of swap-events as the desired behavior varies between softphone implementations.

Consider this scenario:

  • Two calls are ongoing
  • The current call is on hold, meaning both calls are on hold
  • The user presses the flash-button to request a swap.

This scenario is typically handled by resuming the "last current call" (see example below), but we have also seen implementations with swap interpreted as a wish to merge the two held calls. If in doubt, you should ignore swap and handle it through the GUI.

// 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

// User presses flash-button on the device

multiCallControl.swapRequest.subscribe(async () => {
  // Assess the current holdState
  if (multiCallControl.holdState.value === "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 the Start call section, you can subscribe to the number of ongoing calls.

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 will increment by one. It doesn't matter if the ongoing calls are on hold or not - it is a total sum of calls.

At the same time, whenever a call is ended by calling endCall or pressing the end-call button on the device, the count will decrement by one. When the count gets back to 0, the device will end the active call state and return to idle. Read more about ending a call in the single call control section.

It is important that you at all times keep the ongoingCalls count in sync with your softphone's list of ongoing calls.

Change the device used in an active call

The Easy Call Control module uses a device-based abstraction.

Changing the device is a matter of finding the ISingleCallControl or IMultiCallControl instance of the new device that will handle the call, and executing its startCall() or acceptIncomingCall() methods.

Additionally, it is important to inform the old device that it is no longer used in that call - just execute endCall(). If you wish to stop receiving updates for that device, you should also unsubscribe all observables.

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, holState 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();

// 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.
}

Additional details

All Jabra devices have complex firmware that handle many different scenarios. This means that sending a command can have different effects depending on the context that the device is in.

As an example, if you send a mute command to a device but it does not consider itself in a call, the mute command does not have any effect. Similarly, if the user indicates they wish to mute the headset but the device is not in a call state, a mute signal is not sent from the device to the PC.

Additionally, some scenarios can depend on the type of hardware - a feature might not be available on a certain device.

Special cases

Number pad

Some devices come with a numeric keyboard on them for easier dialing, dealing with automated answering systems, or call forwarding. Examples include Jabra Dial 550 and Jabra Pro 9470.

If you use the Chrome Extension transport instead of WebHID, you should be aware that these buttons will not work correctly on Mac OS and Linux. The side effect can be summarized as follows:

This is due to known issues with the HID functionality provided by the system.

Next Steps