2 Jabra Browser Integration
3 https://github.com/gnaudio/jabra-browser-integration
7 Copyright (c) 2017 GN Audio A/S (Jabra)
9 Permission is hereby granted, free of charge, to any person obtaining a copy
10 of this software and associated documentation files (the "Software"), to deal
11 in the Software without restriction, including without limitation the rights
12 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the Software is
14 furnished to do so, subject to the following conditions:
16 The above copyright notice and this permission notice shall be included in all
17 copies or substantial portions of the Software.
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 * The global jabra object is your entry for the jabra browser SDK.
33 * Version of this javascript api (should match version number in file apart from possible alfa/beta designator).
35 export const apiVersion = "2.0.1";
38 * Is the current version a beta ?
40 const isBeta = apiVersion.includes("beta");
43 * Id of proper (production) release of browser plugin.
45 const prodExtensionId = "okpeabepajdgiepelmhkfhkjlhhmofma";
48 * Id of beta release of browser plugin.
50 const betaExtensionId = "igcbbdnhomedfadljgcmcfpdcoonihfe";
53 * Contains information about installed components.
55 export interface InstallInfo {
56 installationOk: boolean;
57 version_chromehost: string;
58 version_nativesdk: string;
59 version_browserextension: string;
60 version_jsapi: string;
61 browserextension_id: string;
62 browserextension_type: string;
66 * Contains information about a device
68 export interface DeviceInfo {
71 deviceConnection: number;
72 deviceFeatures: ReadonlyArray<DeviceFeature>;
75 isInFirmwareUpdateMode: boolean;
77 serialNumber?: string,
80 skypeCertified: boolean;
81 firmwareVersion?: string;
82 electricSerialNumbers?: ReadonlyArray<string>;
83 batteryLevelInPercent?: number;
84 batteryCharging?: boolean;
86 leftEarBudStatus?: boolean;
87 equalizerEnabled?: boolean;
91 * Set to ID of related dongle and/or headset if both are paired and connected.
93 connectedDeviceID?: number;
96 * Set if the same device is connected in more than one way (BT and USB), so
97 * the device appears twice.
99 aliasDeviceID?: number;
102 * Only available in debug versions.
104 parentInstanceId?: string;
107 * Only available in debug versions.
109 usbDevicePath?: string;
112 * Browser media device information group (browser session specific).
113 * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
115 browserGroupId?: string;
118 * The browser's unique identifier for the input (e.g. microphone) part of the Jabra device (page origin specific).
119 * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
121 browserAudioInputId?: string;
124 * The browser's unique identifier for an output (e.g. speaker) part of the Jabra device (page origin specific).
125 * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
127 browserAudioOutputId?: string;
130 * The browser's textual descriptor of the device.
131 * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
133 browserLabel?: string;
137 * A combination of a media stream and information of the associated device from the view of the browser.
139 export interface MediaStreamAndDeviceInfoPair {
141 deviceInfo: DeviceInfo
145 * Names of command response events.
147 const commandEventsList = [
155 "setremotemmilightaction"
159 * All possible device events as discriminative union.
161 export type EventName = "mute" | "unmute" | "device attached" | "device detached" | "acceptcall"
162 | "endcall" | "reject" | "flash" | "online" | "offline" | "linebusy" | "lineidle"
163 | "redial" | "key0" | "key1" | "key2" | "key3" | "key4" | "key5"
164 | "key6" | "key7" | "key8" | "key9" | "keyStar" | "keyPound"
165 | "keyClear" | "Online" | "speedDial" | "voiceMail" | "LineBusy"
166 | "outOfRange" | "intoRange" | "pseudoAcceptcall" | "pseudoEndcall"
167 | "button1" | "button2" | "button3" | "volumeUp" | "volumeDown" | "fireAlarm"
168 | "jackConnection" | "jackDisConnection" | "qdConnection" | "qdDisconnection"
169 | "headsetConnection" | "headsetDisConnection" | "devlog" | "busylight"
170 | "hearThrough" | "batteryStatus" | "gnpButton" | "mmi" | "error";
173 * All possible device events as internal array.
175 let eventNamesList: ReadonlyArray<EventName>
176 = [ "mute", "unmute", "device attached", "device detached", "acceptcall",
177 "endcall", "reject", "flash", "online", "offline", "linebusy", "lineidle",
178 "redial", "key0", "key1", "key2", "key3", "key4", "key5",
179 "key6", "key7", "key8", "key9", "keyStar", "keyPound",
180 "keyClear", "Online", "speedDial", "voiceMail", "LineBusy",
181 "outOfRange", "intoRange", "pseudoAcceptcall", "pseudoEndcall",
182 "button1", "button2", "button3", "volumeUp", "volumeDown", "fireAlarm",
183 "jackConnection", "jackDisConnection", "qdConnection", "qdDisconnection",
184 "headsetConnection","headsetDisConnection", "devlog", "busylight",
185 "hearThrough", "batteryStatus", "gnpButton", "mmi", "error" ];
188 * Error status codes returned by SDK. Same as Jabra_ErrorStatus in native SDK.
190 export enum ErrorCodes {
199 FileNotAccessible = 8,
200 FileNotCompatible = 9,
201 Device_NotFound = 10,
203 Authorization_failed = 12,
204 FileNotAvailable = 13,
205 ConfigParseError = 14,
206 SetSettings_Fail = 15,
208 Device_ReadFail = 17,
209 Device_NotReady = 18,
210 FilePartiallyCompatible = 19
214 * Error return codes. Same as Jabra_ReturnCode in native SDK.
216 export enum ErrorReturnCodes {
221 Return_ParameterFail = 4,
222 ProtectedSetting_Write = 5,
224 NetworkRequest_Fail = 7,
225 Device_WriteFail = 8,
226 Device_ReadFails = 9,
227 No_FactorySupported = 10,
229 Device_BadState = 12,
231 File_AlreadyExists = 14,
232 File_Not_Accessible = 15,
233 Firmware_UpToDate = 16,
234 Firmware_Available = 17,
236 Invalid_Authorization = 19,
237 FWU_Application_Not_Available = 20,
238 Device_AlreadyConnected = 21,
239 Device_NotConnected = 22,
240 CannotClear_DeviceConnected = 23,
241 Device_Rebooted = 24,
242 Upload_AlreadyInProgress = 25,
243 Download_AlreadyInProgress = 26
247 * Custom error returned by commands expecting results when failing.
249 export class CommandError extends Error {
254 constructor(command: string, errmessage: string, data?: string) {
255 super("Command " + command +" failed with error message " + errmessage + " and details: " + JSON.stringify(data || {}));
256 this.command = command;
257 this.errmessage = errmessage;
259 this.name = 'CommandError';
265 * Internal helper that stores information about the promise to resolve/reject
266 * for a command being processed.
268 interface PromiseCallbacks {
270 resolve: (value?: any | PromiseLike<any> | undefined) => void;
271 reject: (err: Error) => void;
275 * Event type for call backs.
277 export interface Event {
286 * The format of errors returned.
288 export type ClientError = any | {
293 * The format of messages returned.
295 export type ClientMessage = any | {
300 * Type for event callback functions..
302 export declare type EventCallback = (event: Event) => void;
305 * Internal mapping from all known events to array of registered callbacks. All possible events are setup
306 * initially. Callbacks values are configured at runtime.
308 const eventListeners: Map<EventName, Array<EventCallback>> = new Map<EventName, Array<EventCallback>>();
309 eventNamesList.forEach((event: EventName) => eventListeners.set(event, []));
312 * Device feature codes.
314 export enum DeviceFeature {
319 MusicEqualizer = 1004,
320 EarbudInterconnectionStatus = 1005,
324 RingtoneUpload = 1009,
326 NeedsExplicitRebootAfterOta = 1011,
327 NeedsToBePutIncCradleToCompleteFwu = 1012,
330 PreferredSoftphoneListInDevice = 1015,
331 VoiceAssistant = 1016,
336 * A specification of a button for MMI capturing.
338 export enum RemoteMmiType {
341 MMI_TYPE_VOLDOWN = 2,
344 MMI_TYPE_TR_FORW = 5,
345 MMI_TYPE_TR_BACK = 6,
348 MMI_TYPE_HOOK_OFF = 9,
349 MMI_TYPE_HOOK_ON = 10,
350 MMI_TYPE_BLUETOOTH = 11,
352 MMI_TYPE_BATTERY = 13,
356 MMI_TYPE_LISTEN_IN = 17,
363 * A MMI effect specification for light on, off or blinking in different tempo.
365 export enum RemoteMmiSequence {
366 MMI_LED_SEQUENCE_OFF = 0,
367 MMI_LED_SEQUENCE_ON = 1,
368 MMI_LED_SEQUENCE_SLOW = 2,
369 MMI_LED_SEQUENCE_FAST = 3
373 * MMI button actions reported when button has focus.
375 export enum RemoteMmiActionInput {
379 MMI_ACTION_DOUBLE_TAP = 8,
380 MMI_ACTION_PRESS = 16,
381 MMI_ACTION_LONG_PRESS = 32,
382 MMI_ACTION_X_LONG_PRESS = 64
386 * A 3 x 8 bit set of RGB colors. Numbers can be between 0-255.
388 export type ColorType = [number, number, number];
391 * The log level currently used internally in this api facade. Initially this is set to show errors and
392 * warnings until a logEvent (>=0.5) changes this when initializing the extension or when the user
393 * changes the log level. Available in the API for testing only - do not use this in normal applications.
395 export let logLevel: number = 2;
398 * An internal logger helper.
400 const logger = new class {
427 * A reasonably unique ID for our browser extension client that makes it possible to
428 * differentiate between different instances of this api in different browser tabs.
430 const apiClientId: string = Math.random().toString(36).substr(2, 9);
433 * A mapping from unique request ids for commands and the promise information needed
434 * to resolve/reject them by an incomming event.
436 const sendRequestResultMap: Map<string, PromiseCallbacks> = new Map<string, PromiseCallbacks>();
439 * A counter used to generate unique request ID's used to match commands and returning events.
441 let requestNumber: number = 1;
444 * Contains initialization information used by the init/shutdown methods.
447 initialized?: boolean;
448 initializing?: boolean;
449 eventCallback?: (event: any) => void;
453 * The JavaScript library must be initialized using this function. It returns a promise that
454 * resolves when initialization is complete.
456 export function init(): Promise<void> {
457 return new Promise((resolve, reject) => {
458 // Only Chrome is currently supported
459 let isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
461 return reject(new Error("Jabra Browser Integration: Only supported by <a href='https://google.com/chrome'>Google Chrome</a>."));
464 if (initState.initialized || initState.initializing) {
465 return reject(new Error("Jabra Browser Integration already initialized"));
468 initState.initializing = true;
469 sendRequestResultMap.clear();
470 let duringInit = true;
472 initState.eventCallback = (event: any) => {
473 if (event.source === window &&
474 event.data.direction &&
475 event.data.direction === "jabra-headset-extension-from-content-script") {
477 let eventApiClientId = event.data.apiClientId || "";
478 let requestId = event.data.requestId || "";
480 // Only accept responses from our own requests or from device.
481 if (apiClientId === eventApiClientId || eventApiClientId === "") {
482 logger.trace("Receiving event from content script: " + JSON.stringify(event.data));
484 // For backwards compatibility a blank message might be send as "na".
485 if (event.data.message === "na") {
486 delete event.data.message;
489 // For backward compatability reinterprent messages starting with error as errors:
490 if (event.data.message && event.data.message.startsWith("Error:")) {
491 event.data.error = event.data.message;
492 delete event.data.message
495 if (event.data.message) {
496 logger.trace("Got message: " + JSON.stringify(event.data));
497 const normalizedMsg: string = event.data.message.substring(7); // Strip "Event" prefix;
499 if (normalizedMsg.startsWith("logLevel")) {
500 logLevel = parseInt(event.data.message.substring(16));
501 logger.trace("Logger set to level " + logLevel);
503 // Loglevels are internal events and not an indication of proper
504 // initialization so skip rest of handling for log levels.
508 const commandIndex = commandEventsList.findIndex((e) => normalizedMsg.startsWith(e));
509 if (commandIndex >= 0) {
510 // For install info and version command, we need to add api version number.
511 if (normalizedMsg === "getinstallinfo" || (normalizedMsg.startsWith("Version "))) {
512 // Old extension/host won't have data so make sure it exists to avoid breakage.
513 if (!event.data.data) {
514 event.data.data = {};
516 event.data.data.version_jsapi = apiVersion;
519 // For install info also check if the full installation is consistent.
520 if (normalizedMsg === "getinstallinfo") {
521 event.data.data.installationOk = isInstallationOk(event.data.data);
524 // Lookup and check that we have identified a (real) command target to pair result with.
525 let resultTarget = identifyAndCleanupResultTarget(requestId);
528 if (event.data.data) {
529 result = event.data.data;
531 let dataPosition = commandEventsList[commandIndex].length + 1;
532 let dataStr = normalizedMsg.substring(dataPosition);
535 result.legacy_result = dataStr;
539 resultTarget.resolve(result)
541 let err = "Result target information missing for message " + event.data.message + ". This is likely due to some software components that have not been updated or a software bug. Please upgrade extension and/or chromehost";
545 message: event.data.message
548 } else if (eventListeners.has(normalizedMsg as EventName)) {
549 let clientEvent: ClientMessage = JSON.parse(JSON.stringify(event.data));
550 delete clientEvent.direction;
551 delete clientEvent.apiClientId;
552 delete clientEvent.requestId;
553 clientEvent.message = normalizedMsg;
555 notify(normalizedMsg as EventName, clientEvent);
557 logger.warn("Unknown message: " + event.data.message);
559 error: "Unknown message: ",
560 message: event.data.message
562 // Don't let unknown messages complete initialization so stop here.
570 } else if (event.data.error) {
571 logger.error("Got error: " + event.data.error);
572 const normalizedError: string = event.data.error.substring(7); // Strip "Error" prefix;
574 // Reject target promise if there is one - otherwise send a general error.
575 let resultTarget = identifyAndCleanupResultTarget(requestId);
577 resultTarget.reject(new CommandError(resultTarget.cmd, normalizedError, event.data.data));
579 let clientError: ClientError = JSON.parse(JSON.stringify(event.data));
580 delete clientError.direction;
581 delete clientError.apiClientId;
582 delete clientError.requestId;
583 clientError.error = normalizedError;
585 notify("error", clientError);
590 return reject(new Error(event.data.error));
597 window.addEventListener("message", initState.eventCallback!);
599 // Initial getversion and loglevel.
602 sendCmdWithResult("getversion", null, false).then((result) => {
603 let resultStr = (typeof result === 'string' || result instanceof String) ? result : JSON.stringify(result, null, 2);
604 logger.trace("getversion returned successfully with : " + resultStr);
606 sendCmd("logLevel", null, false);
607 }).catch((error) => {
614 // Check if the web-extension is installed
617 if (duringInit === true) {
619 const extensionId = isBeta ? betaExtensionId : prodExtensionId;
620 reject(new Error("Jabra Browser Integration: You need to use this <a href='https://chrome.google.com/webstore/detail/" + extensionId + "'>Extension</a> and then reload this page"));
627 * Helper that checks if the installation is consistent.
629 function isInstallationOk(installInfo: InstallInfo): boolean {
630 let browserSdkVersions = [installInfo.version_browserextension, installInfo.version_chromehost, installInfo.version_jsapi];
632 // Check that we have install information for all components.
633 if (browserSdkVersions.some(v => !v) || !installInfo.version_nativesdk) {
637 // Check that different beta versions are not mixed.
638 if (!browserSdkVersions.map(v => {
639 let betaIndex = v.lastIndexOf('beta');
640 if (betaIndex>=0 && v.length>betaIndex+4) {
641 return v.substr(betaIndex+4);
645 }).filter(v => v).every((v, i, arr) => v === arr[0])) {
653 * Post event/error to subscribers.
655 function notify(eventName: EventName, eventMsg: ClientMessage | ClientError): void {
656 let callbacks = eventListeners.get(eventName);
658 callbacks.forEach((callback) => {
662 // This should not occur unless internal event mappings in this file
663 // are not configured correctly.
664 logger.error("Unexpected unknown eventName: " + eventName);
668 /** Lookup any previous stored result target information for the request.
669 * Does cleanup if target found (so it can't be called twice for a request).
670 * Nb. requestId's are only provided by >= 0.5 extension and chromehost.
672 function identifyAndCleanupResultTarget(requestId?: string) : PromiseCallbacks | undefined {
673 // Lookup any previous stored result target information for the request.
674 // Nb. requestId's are only provided by >= 0.5 extension and chromehost.
675 let resultTarget: PromiseCallbacks | undefined;
677 resultTarget = sendRequestResultMap.get(requestId);
678 // Remember to cleanup to avoid memory leak!
679 sendRequestResultMap.delete(requestId);
680 } else if (sendRequestResultMap.size === 1) {
681 // We don't have a requestId but since only one is being executed we
682 // can assume this is the one.
683 let value = sendRequestResultMap.entries().next().value;
684 resultTarget = value[1];
685 // Remember to cleanup to avoid memory leak and for future
686 // requests like this to be resolved.
687 sendRequestResultMap.delete(value[0]);
689 // No idea what target matches what request - give up.
690 resultTarget = undefined;
693 // Warn in case of likely memory leak:
694 const mapSize = sendRequestResultMap.size;
695 if (mapSize > 10 && mapSize % 10 === 0) { // Limit warnings to every 10 size increases to avoid flooding:
696 logger.warn("Memory leak found - Request result map is getting too large (size #" + mapSize + ")");
703 initState.initialized = true;
704 initState.initializing = false;
709 * De-initialize the api after use. Not normally used as api will normally
710 * stay in use thoughout an application - mostly of interest for testing.
712 export function shutdown(): Promise<void> {
713 if (initState.initialized) {
714 window.removeEventListener("message", initState.eventCallback!);
715 initState.eventCallback = undefined;
716 sendRequestResultMap.clear();
718 initState.initialized = false;
721 eventListeners.forEach((value, key) => {
724 return Promise.resolve();
727 return Promise.reject(new Error("Browser integration not initialized"));
731 * Internal helper that returns an array of valid event keys that correspond to the event specificator
732 * and are known to exist in our event listener map.
734 function getEvents(nameSpec: string | RegExp | Array<string | RegExp>): ReadonlyArray<string> {
735 if (Array.isArray(nameSpec)) {
736 // @ts-ignore: Disable wrong "argument not assignable" error in ts 3.4
737 return [ ...new Set<string>([].concat.apply([], nameSpec.map(a => getEvents(a)))) ];
738 } else if (nameSpec instanceof RegExp) {
739 return Array.from<string>(eventListeners.keys()).filter(key => nameSpec.test(key))
741 if (eventListeners.has(nameSpec as EventName)) {
744 logger.warn("Unknown event " + nameSpec + " ignored when adding/removing eventlistener");
752 * Hook up listener call back to specified event(s) as specified by initial name specification argument nameSpec.
753 * When the nameSpec argument is a string, this correspond to a single named event. When the argument is a regular
754 * expression all lister subscribes to all matching events. If the argument is an array it recursively subscribes
755 * to all events specified in the array.
757 export function addEventListener(nameSpec: string | RegExp | Array<string | RegExp>, callback: EventCallback): void {
758 getEvents(nameSpec).map(name => {
759 let callbacks = eventListeners.get(name as EventName);
760 if (!callbacks!.find((c) => c === callback)) {
761 callbacks!.push(callback);
767 * Remove existing listener to specified event(s). The callback must correspond to the exact callback provided
768 * to a previous addEventListener.
770 export function removeEventListener(nameSpec: string | RegExp | Array<string | RegExp>, callback: EventCallback): void {
771 getEvents(nameSpec).map(name => {
772 let callbacks = eventListeners.get(name as EventName);
773 let findIndex = callbacks!.findIndex((c) => c === callback);
774 if (findIndex >= 0) {
775 callbacks!.splice(findIndex, 1);
781 * Activate ringer (if supported) on the Jabra Device
783 export function ring(): void {
788 * Change state to in-a-call.
790 export function offHook(): void {
795 * Change state to idle (not-in-a-call).
797 export function onHook(): void {
802 * Mutes the microphone (if supported).
804 export function mute(): void {
809 * Unmutes the microphone (if supported).
811 export function unmute(): void {
816 * Change state to held (if supported).
818 export function hold(): void {
823 * Change state from held to OffHook (if supported).
825 export function resume(): void {
830 * Capture/release buttons for customization (if supported). This turns off default behavior and enables mmi events to
831 * be received instead. It also allows for mmi actions to be applied like changing lights with setRemoteMmiLightAction.
833 * @param type The button that should be captured/released.
834 * @param capture True if button should be captured, false if it should be released.
836 * @returns A promise that is resolved once operation completes.
838 export function setMmiFocus(type: RemoteMmiType | string, capture: boolean | string): Promise<void> {
839 let typeVal = numberOrString(type);
840 let captureVal = booleanOrString(capture);
841 return sendCmdWithResult<void>("setmmifocus", {
848 * Change light/color on a previously captured button.
849 * Nb. This requires the button to be previously captured though setMMiFocus.
851 * @param type The button that should be captured/released.
852 * @param color An RGB array of 3x integers or a RGB number (with 0x or # prefix for hex).
853 * @param effect What effect to apply to the button.
855 * @returns A promise that is resolved once operation completes.
857 export function setRemoteMmiLightAction(type: RemoteMmiType | string, color: ColorType | string | number, effect: RemoteMmiSequence | string): Promise<void> {
858 let typeVal = numberOrString(type);
859 let colorVal = colorOrString(color);
860 let effectVal = numberOrString(effect);
861 return sendCmdWithResult<void>("setremotemmilightaction", {
869 * Internal helper to get detailed information about the current active Jabra Device
870 * from SDK, including current status but excluding media device information.
872 function _doGetActiveSDKDevice(): Promise<DeviceInfo> {
873 return sendCmdWithResult<DeviceInfo>("getactivedevice");
877 * Internal helper to get detailed information about the all attached Jabra Devices
878 * from SDK, including current status but excluding media device information.
880 function _doGetSDKDevices(): Promise<ReadonlyArray<DeviceInfo>> {
881 return sendCmdWithResult<ReadonlyArray<DeviceInfo>>("getdevices");
885 * Get detailed information about the current active Jabra Device, including current status
886 * and optionally also including related browser media device information.
888 * Note that browser media device information requires mediaDevices.getUserMedia or
889 * getUserDeviceMediaExt to have been called so permissions are granted. Browser media information
890 * is useful for setting a device constraint on mediaDevices.getUserMedia for input or for calling
891 * setSinkId (when supported by the browser) to set output.
893 export function getActiveDevice(includeBrowserMediaDeviceInfo: boolean | string = false): Promise<DeviceInfo> {
894 let includeBrowserMediaDeviceInfoVal = booleanOrString(includeBrowserMediaDeviceInfo);
895 if (includeBrowserMediaDeviceInfoVal) {
896 return _doGetActiveSDKDevice_And_BrowserDevice();
898 return _doGetActiveSDKDevice();
903 * List detailed information about all attached Jabra Devices, including current status.
904 * and optionally also including related browser media device information.
906 * Note that browser media device information requires mediaDevices.getUserMedia or
907 * getUserDeviceMediaExt to have been called so permissions are granted. Browser media information
908 * is useful for setting a device constraint on mediaDevices.getUserMedia for input or for calling
909 * setSinkId (when supported by the browser) to set output.
911 export function getDevices(includeBrowserMediaDeviceInfo: boolean | string = false): Promise<ReadonlyArray<DeviceInfo>> {
912 let includeBrowserMediaDeviceInfoVal = booleanOrString(includeBrowserMediaDeviceInfo);
913 if (includeBrowserMediaDeviceInfoVal) {
914 return _doGetSDKDevices_And_BrowserDevice();
916 return _doGetSDKDevices();
921 * Internal utility that select a new active device in a backwards compatible way that works with earlier chrome host.
922 * Used internally by test tool - do not use otherwise.
924 * Note: The active device is a global setting that affects all browser
925 * instances using the browser SDK. Unless changed specifically, the setting
926 * persist until browser is restarted or device is unplugged.
928 * @deprecated Use setActiveDeviceId instead.
930 export function _setActiveDeviceId(id: number | string): void {
931 let idVal = numberOrString(id);
933 // Use both new and old way of passing parameters for compatibility with <= v0.5.
934 sendCmd("setactivedevice " + id.toString(), { id: idVal } );
938 * Select a new active device returning once selection is completed.
940 * Note: The active device is a global setting that affects all browser
941 * instances using the browser SDK. Unless changed specifically, the setting
942 * persist until browser is restarted or device is unplugged.
944 * @param id The id number of the new active device.
945 * @returns A promise that is resolved once selection completes.
948 export function setActiveDeviceId(id: number | string): Promise<void> {
949 let idVal = numberOrString(id);
951 return sendCmdWithResult<void>("setactivedevice2", { id: idVal } );
955 * Set busylight on active device (if supported)
957 * @param busy True if busy light should be set, false if it should be cleared.
959 export function setBusyLight(busy: boolean | string): Promise<void> {
960 let busyVal = booleanOrString(busy);
962 return sendCmdWithResult<void>("setbusylight", { busy: busyVal } );
966 * Get version number information for all components.
968 export function getInstallInfo(): Promise<InstallInfo> {
969 return sendCmdWithResult<InstallInfo>("getinstallinfo");
973 * Internal helper that forwards a command to the browser extension
974 * without expecting a response.
976 function sendCmd(cmd: string, args: object | null = null, requireInitializedCheck: boolean = true): void {
977 if (!requireInitializedCheck || (requireInitializedCheck && initState.initialized)) {
978 let requestId = (requestNumber++).toString();
981 direction: "jabra-headset-extension-from-page-script",
984 requestId: requestId,
985 apiClientId: apiClientId,
986 version_jsapi: apiVersion
989 logger.trace("Sending command to content script: " + JSON.stringify(msg));
991 window.postMessage(msg, "*");
993 throw new Error("Browser integration not initialized");
998 * Internal helper that forwards a command to the browser extension
999 * expecting a response (a promise).
1001 function sendCmdWithResult<T>(cmd: string, args: object | null = null, requireInitializedCheck: boolean = true): Promise<T> {
1002 if (!requireInitializedCheck || (requireInitializedCheck && initState.initialized)) {
1003 let requestId = (requestNumber++).toString();
1005 return new Promise<T>((resolve, reject) => {
1006 sendRequestResultMap.set(requestId, { cmd, resolve, reject });
1009 direction: "jabra-headset-extension-from-page-script",
1012 requestId: requestId,
1013 apiClientId: apiClientId,
1014 version_jsapi: apiVersion
1017 logger.trace("Sending command to content script expecting result: " + JSON.stringify(msg));
1019 window.postMessage(msg, "*");
1022 return Promise.reject(new Error("Browser integration not initialized"));
1027 * Configure an audio html element on a webpage to use jabra audio device as speaker output. Returns a promise with boolean success status.
1028 * The deviceInfo argument must come from getDeviceInfo or getUserDeviceMediaExt calls.
1030 export function trySetDeviceOutput(audioElement: HTMLMediaElement, deviceInfo: DeviceInfo): Promise<boolean> {
1031 if (!audioElement || !deviceInfo) {
1032 return Promise.reject(new Error('Call to trySetDeviceOutput has argument(s) missing'));
1035 if (!(typeof ((audioElement as any).setSinkId) === "function")) {
1036 return Promise.reject(new Error('Your browser does not support required Audio Output Devices API'));
1039 return (audioElement as any).setSinkId(deviceInfo.browserAudioOutputId).then(() => {
1040 var success = (audioElement as any).sinkId === deviceInfo.browserAudioOutputId;
1046 * Checks if a Jabra Input device is in fact selected in a media stream.
1047 * The deviceInfo argument must come from getDeviceInfo or getUserDeviceMediaExt calls.
1049 export function isDeviceSelectedForInput(mediaStream: MediaStream, deviceInfo: DeviceInfo): boolean {
1050 if (!mediaStream || !deviceInfo) {
1051 throw Error('Call to isDeviceSelectedForInput has argument(s) missing');
1054 var tracks = mediaStream.getAudioTracks();
1055 for (var i = 0, len = tracks.length; i < len; i++) {
1056 var track = tracks[i];
1057 var trackCap = track.getCapabilities();
1058 if (trackCap.deviceId !== deviceInfo.browserAudioInputId) {
1067 * Replacement for mediaDevices.getUserMedia that makes a best effort to select the active Jabra audio device
1068 * to be used for the microphone. Unlike getUserMedia this method returns a promise that
1069 * resolve to an object containing both a stream and the device info for the selected device.
1071 * Optional, additional non-audio constrains (like f.x. video) can be specified as well.
1073 * Note: Subsequently, if this method appears to succeed use the isDeviceSelectedForInput function to check
1074 * if the browser did in fact choose a Jabra device for the microphone.
1076 export function getUserDeviceMediaExt(constraints?: MediaStreamConstraints): Promise<MediaStreamAndDeviceInfoPair> {
1077 // Good error if using old browser:
1078 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
1079 return Promise.reject(new Error('Your browser does not support required media api'));
1083 if (!initState.initialized) {
1084 return Promise.reject(new Error("Browser integration not initialized"));
1087 // Warn of degraded UX experience unless we are running https.
1088 if (location.protocol !== 'https:') {
1089 logger.warn("This function needs to run under https for best UX experience (persisted permissions)");
1092 // Check input validity:
1093 if (constraints !== undefined && constraints !== null && typeof constraints !== 'object') {
1094 return Promise.reject(new Error("Optional constraints parameter must be an object"));
1098 * Utility method that combines constraints with ours taking precedence (deep).
1100 function mergeConstraints(ours: MediaStreamConstraints, theirs?: MediaStreamConstraints): MediaStreamConstraints {
1101 if (theirs !== null && theirs !== undefined && typeof ours === 'object') {
1102 let result: { [index: string]: any } = {};
1103 for (var attrname in theirs) { result[attrname] = (theirs as any)[attrname]; }
1104 for (var attrname in ours) { result[attrname] = mergeConstraints((ours as any)[attrname], (theirs as any)[attrname]); } // Ours takes precedence.
1111 // If we have the input device id already we can do a direct call to getUserMedia, otherwise we have to do
1112 // an initial general call to getUserMedia just get access to looking up the input device and then a second
1113 // call to getUserMedia to make sure the Jabra input device is selected.
1114 return navigator.mediaDevices.getUserMedia(mergeConstraints({ audio: true }, constraints)).then((dummyStream) => {
1115 return _doGetActiveSDKDevice_And_BrowserDevice().then((deviceInfo) => {
1116 // Shutdown initial dummy stream (not sure it is really required but let's be nice).
1117 dummyStream.getTracks().forEach((track) => {
1121 if (deviceInfo && deviceInfo.browserAudioInputId) {
1122 return navigator.mediaDevices.getUserMedia(mergeConstraints({ audio: { deviceId: deviceInfo.browserAudioInputId } }, constraints))
1126 deviceInfo: deviceInfo
1130 return Promise.reject(new Error('Could not find a Jabra device with a microphone'));
1137 * Internal helper for add media information properties to existing SDK device information.
1139 function fillInMatchingMediaInfo(deviceInfo: DeviceInfo, mediaDevices: MediaDeviceInfo[]): void {
1140 function findBestMatchIndex(sdkDeviceName: string, mediaDeviceNameCandidates: string[]): number {
1141 // Edit distance helper adapted from
1142 // https://stackoverflow.com/questions/10473745/compare-strings-javascript-return-of-likely
1143 function editDistance(s1: string, s2: string) {
1144 s1 = s1.toLowerCase();
1145 s2 = s2.toLowerCase();
1147 var costs = new Array();
1148 for (var i = 0; i <= s1.length; i++) {
1150 for (var j = 0; j <= s2.length; j++) {
1155 var newValue = costs[j - 1];
1156 if (s1.charAt(i - 1) != s2.charAt(j - 1))
1157 newValue = Math.min(Math.min(newValue, lastValue),
1159 costs[j - 1] = lastValue;
1160 lastValue = newValue;
1165 costs[s2.length] = lastValue;
1167 return costs[s2.length];
1170 // Levenshtein distance helper adapted from
1171 // https://stackoverflow.com/questions/10473745/compare-strings-javascript-return-of-likely
1172 function levenshteinDistance(s1: string, s2: string) : number {
1175 if (s1.length < s2.length) {
1179 let longerLength = longer.length;
1180 if (longerLength === 0) {
1183 return (longerLength - editDistance(longer, shorter)) / longerLength;
1186 if (mediaDeviceNameCandidates.length == 1) {
1188 } else if (mediaDeviceNameCandidates.length > 0) {
1189 let similarities = mediaDeviceNameCandidates.map(candidate => {
1190 if (candidate.includes("(" + sdkDeviceName + ")")) {
1193 // Remove Standard/Default prefix from label in Chrome when comparing
1194 let prefixEnd = candidate.indexOf(' - ');
1195 let cleanedCandidate = (prefixEnd >= 0) ? candidate.substring(prefixEnd + 3) : candidate;
1197 return levenshteinDistance(sdkDeviceName, cleanedCandidate)
1200 let bestMatchIndex = similarities.reduce((prevIndexMax, value, i, a) => value > a[prevIndexMax] ? i : prevIndexMax, 0);
1201 return bestMatchIndex;
1207 // Find matching pair input or output device.
1208 function findMatchingMediaDevice(groupId: string, kind: string, src: MediaDeviceInfo[]): MediaDeviceInfo | undefined {
1209 return src.find(md => md.groupId == groupId && md.kind == kind);
1212 if (deviceInfo && deviceInfo.deviceName) {
1213 let groupId: string | undefined = undefined;
1214 let audioInputId: string | undefined = undefined;
1215 let audioOutputId: string | undefined = undefined;
1216 let label: string | undefined = undefined;
1217 // Filter out non Jabra input/output devices:
1218 let jabraMediaDevices = mediaDevices.filter(device => device.label
1219 && device.label.toLowerCase().includes('jabra')
1220 && (device.kind === 'audioinput' || device.kind === 'audiooutput'));
1221 let someJabraDeviceIndex = findBestMatchIndex(deviceInfo.deviceName, jabraMediaDevices.map(md => md.label));
1222 if (someJabraDeviceIndex >= 0) {
1223 let foundDevice = jabraMediaDevices[someJabraDeviceIndex];
1224 groupId = foundDevice.groupId;
1225 label = foundDevice.label;
1226 if (foundDevice.kind === 'audioinput') {
1227 audioInputId = foundDevice.deviceId;
1228 // Lookup matching output device:
1229 let outputDevice = findMatchingMediaDevice(groupId, 'audiooutput', jabraMediaDevices);
1231 audioOutputId = outputDevice.deviceId;
1234 else if (foundDevice.kind === 'audiooutput') {
1235 audioOutputId = foundDevice.deviceId;
1236 // Lookup matching output input device:
1237 let inputDevice = findMatchingMediaDevice(groupId, 'audioinput', jabraMediaDevices);
1239 audioInputId = inputDevice.deviceId;
1244 deviceInfo.browserGroupId = groupId;
1247 deviceInfo.browserLabel = label;
1250 deviceInfo.browserAudioInputId = audioInputId;
1252 if (audioOutputId) {
1253 deviceInfo.browserAudioOutputId = audioOutputId;
1256 // Do nothing if device information is missing.
1261 * Internal helper that returns complete device information, including both SDK and browser media device
1262 * information for all devices.
1265 * 1) Only works if hosted under https.
1268 * 1) Output devices not supported yet. See "https://bugzilla.mozilla.org/show_bug.cgi?id=934425"
1269 * 2) The user must have provided permission to use the specific device to use it as a constraint.
1270 * 3) GroupId not supported.
1272 * General non-chrome browser note:
1273 * 1) Returning output devices requires support for new Audio Output Devices API.
1275 function _doGetSDKDevices_And_BrowserDevice(): Promise<ReadonlyArray<DeviceInfo>> {
1276 // Good error if using old browser:
1277 if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
1278 return Promise.reject(new Error('Your browser does not support required media api'));
1282 if (!initState.initialized) {
1283 return Promise.reject(new Error("Browser integration not initialized"));
1286 // Browser security rules (for at least chrome) requires site to run under https for labels to be read.
1287 if (location.protocol !== 'https:') {
1288 return Promise.reject(new Error('Your browser needs https for lookup to work'));
1291 return Promise.all([_doGetSDKDevices(), navigator.mediaDevices.enumerateDevices()]).then( ([deviceInfos, mediaDevices]) => {
1292 deviceInfos.forEach( (deviceInfo) => {
1293 fillInMatchingMediaInfo(deviceInfo, mediaDevices);
1301 * Internal helper that returns complete device information, including both SDK and browser media device
1302 * information for active device.
1305 * 1) Only works if hosted under https.
1308 * 1) Output devices not supported yet. See "https://bugzilla.mozilla.org/show_bug.cgi?id=934425"
1309 * 2) The user must have provided permission to use the specific device to use it as a constraint.
1310 * 3) GroupId not supported.
1312 * General non-chrome browser note:
1313 * 1) Returning output devices requires support for new Audio Output Devices API.
1315 function _doGetActiveSDKDevice_And_BrowserDevice(): Promise<DeviceInfo> {
1316 // Good error if using old browser:
1317 if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
1318 return Promise.reject(new Error('Your browser does not support required media api'));
1322 if (!initState.initialized) {
1323 return Promise.reject(new Error("Browser integration not initialized"));
1326 // Browser security rules (for at least chrome) requires site to run under https for labels to be read.
1327 if (location.protocol !== 'https:') {
1328 return Promise.reject(new Error('Your browser needs https for lookup to work'));
1331 // enumerateDevices requires user to have provided permission using getUserMedia for labels to be filled out.
1332 return Promise.all([_doGetActiveSDKDevice(), navigator.mediaDevices.enumerateDevices()]).then( ([deviceInfo, mediaDevices]) => {
1333 fillInMatchingMediaInfo(deviceInfo, mediaDevices);
1339 * Helper that pass boolean values through and parses strings to booleans.
1341 function booleanOrString(arg: boolean | string) : boolean
1343 if (arg !== "" && ((typeof arg === 'string') || ((arg as any) instanceof String))) {
1344 return (arg === 'true' || arg === '1');
1345 } else if (typeof(arg) === "boolean") {
1348 throw new Error("Illegal/missing argument - boolean or string expected");
1353 * Helper that pass numbers through and parses strings to numbers.
1355 function numberOrString(arg: number | string): number {
1356 if (arg !== "" && ((typeof arg === 'string') || ((arg as any) instanceof String))) {
1357 return parseInt(arg as string);
1358 } else if (typeof arg == 'number') {
1361 throw new Error("Illegal/missing argument - number or string expected");
1366 * Helper that pass color array through and converts values to color array.
1368 function colorOrString(arg: ReadonlyArray<number> | number | string): ReadonlyArray<number> {
1369 if (arg !== "" && ((typeof arg === 'string') || ((arg as any) instanceof String))) {
1370 let combinedValue = parseInt(arg as string, 16);
1371 return [ (combinedValue >> 16) & 255, (combinedValue >> 8) & 255, combinedValue & 255 ];
1372 } else if (typeof arg == 'number') {
1373 let combinedValue = arg;
1374 return [ (combinedValue >> 16) & 255, (combinedValue >> 8) & 255, combinedValue & 255 ];
1375 } else if (Array.isArray(arg)) {
1376 if (arg.length !=3) {
1377 throw new Error("Illegal argument - wrong dimension of number array (3 expected)");
1381 throw new Error("Illegal/missing argument - number array or hex string expected");