77d07221711cbf81190ab40baea088d7e0815a40
[motion2.git] / public / assets / plugins / jabra / jabra.browser.integration-2.0.ts
1 /*
2 Jabra Browser Integration
3 https://github.com/gnaudio/jabra-browser-integration
4
5 MIT License
6
7 Copyright (c) 2017 GN Audio A/S (Jabra)
8
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:
15
16 The above copyright notice and this permission notice shall be included in all
17 copies or substantial portions of the Software.
18
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
25 SOFTWARE.
26 */
27
28 /**
29 * The global jabra object is your entry for the jabra browser SDK.
30 */
31 namespace jabra {
32     /**
33      * Version of this javascript api (should match version number in file apart from possible alfa/beta designator).
34      */
35     export const apiVersion = "2.0.1";
36
37     /**
38      * Is the current version a beta ?
39      */
40     const isBeta = apiVersion.includes("beta");
41
42     /**
43      * Id of proper (production) release of browser plugin.
44      */
45     const prodExtensionId = "okpeabepajdgiepelmhkfhkjlhhmofma";
46
47      /**
48      * Id of beta release of browser plugin.
49      */
50     const betaExtensionId = "igcbbdnhomedfadljgcmcfpdcoonihfe";
51
52     /**
53      * Contains information about installed components.
54      */
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;
63     };
64
65     /**
66      * Contains information about a device
67      */
68     export interface DeviceInfo {
69         deviceID: number;
70         deviceName: string;
71         deviceConnection: number;
72         deviceFeatures: ReadonlyArray<DeviceFeature>;
73         errStatus: number;
74         isBTPaired?: boolean;
75         isInFirmwareUpdateMode: boolean;
76         productID: number;
77         serialNumber?: string,
78         variant: string;
79         dongleName?: string;
80         skypeCertified: boolean;
81         firmwareVersion?: string;
82         electricSerialNumbers?: ReadonlyArray<string>;
83         batteryLevelInPercent?: number;
84         batteryCharging?: boolean;
85         batteryLow?: boolean;
86         leftEarBudStatus?: boolean;
87         equalizerEnabled?: boolean;
88         busyLight?: boolean;
89
90         /**
91          * Set to ID of related dongle and/or headset if both are paired and connected.
92          */
93         connectedDeviceID?: number;
94
95         /**
96          * Set if the same device is connected in more than one way (BT and USB), so
97          * the device appears twice.
98          */
99         aliasDeviceID?: number;
100
101         /**
102          * Only available in debug versions.
103          */
104         parentInstanceId?: string;
105
106         /**
107          * Only available in debug versions.
108          */
109         usbDevicePath?: string;
110
111         /**
112          * Browser media device information group (browser session specific).
113          * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
114          */
115         browserGroupId?: string;
116
117         /**
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.
120          */
121         browserAudioInputId?: string;
122
123          /**
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.
126          */
127         browserAudioOutputId?: string;
128
129          /**
130          * The browser's textual descriptor of the device.
131          * Only available when calling getDevices/getActiveDevice with includeBrowserMediaDeviceInfo argument set to true.
132          */
133         browserLabel?: string;
134     };
135
136     /**
137      * A combination of a media stream and information of the associated device from the view of the browser.
138      */
139     export interface MediaStreamAndDeviceInfoPair {
140         stream: MediaStream;
141         deviceInfo: DeviceInfo
142     };
143
144     /**
145      * Names of command response events.
146      */
147     const commandEventsList = [
148         "devices",
149         "activedevice",
150         "getinstallinfo",
151         "Version",
152         "setmmifocus",
153         "setactivedevice2",
154         "setbusylight",
155         "setremotemmilightaction"
156     ];
157
158     /**
159      * All possible device events as discriminative  union.
160      */
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";
171
172     /**
173      * All possible device events as internal array.
174      */
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" ];
186
187     /**
188      * Error status codes returned by SDK. Same as Jabra_ErrorStatus in native SDK.
189      */
190     export enum ErrorCodes {
191         NoError = 0,
192         SSLError = 1,
193         CertError = 2,
194         NetworkError = 3,
195         DownloadError = 4,
196         ParseError = 5,
197         OtherError = 6,
198         DeviceInfoError = 7,
199         FileNotAccessible = 8,
200         FileNotCompatible = 9,
201         Device_NotFound = 10,
202         Parameter_fail = 11,
203         Authorization_failed = 12,
204         FileNotAvailable = 13,
205         ConfigParseError = 14,
206         SetSettings_Fail = 15,
207         Device_Reboot = 16,
208         Device_ReadFail = 17,
209         Device_NotReady = 18,
210         FilePartiallyCompatible = 19
211     };
212
213     /**
214      * Error return codes. Same as Jabra_ReturnCode in native SDK.
215      */
216     export enum ErrorReturnCodes {
217        Return_Ok = 0,
218        Device_Unknown = 1,
219        Device_Invalid = 2,
220        Not_Supported = 3,
221        Return_ParameterFail = 4,
222        ProtectedSetting_Write = 5,
223        No_Information = 6,
224        NetworkRequest_Fail = 7,
225        Device_WriteFail = 8,
226        Device_ReadFails = 9,
227        No_FactorySupported = 10,
228        System_Error = 11,
229        Device_BadState = 12,
230        FileWrite_Fail = 13,
231        File_AlreadyExists = 14,
232        File_Not_Accessible = 15,
233        Firmware_UpToDate = 16,
234        Firmware_Available = 17,
235        Return_Async = 18,
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
244     };
245
246     /**
247      * Custom error returned by commands expecting results when failing.
248      */
249     export class CommandError extends Error {
250         command: string;
251         errmessage: string;
252         data: any;
253
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;
258             this.data = data;
259             this.name = 'CommandError';
260         }
261     };
262
263
264     /**
265      * Internal helper that stores information about the promise to resolve/reject
266      * for a command being processed.
267      */
268     interface PromiseCallbacks {
269         cmd: string,
270         resolve: (value?: any | PromiseLike<any> | undefined) => void;
271         reject: (err: Error) => void;
272     }
273
274     /**
275      * Event type for call backs.
276      */
277     export interface Event {
278         message: string;
279         data: {
280             deviceID: number;
281             /* variable */
282         };
283     };
284
285     /**
286      * The format of errors returned.
287      */
288     export type ClientError = any | {
289         error: string;
290     };
291
292      /**
293      * The format of messages returned.
294      */
295     export type ClientMessage = any | {
296         message: string;
297     };    
298     
299     /**
300      * Type for event callback functions..
301      */
302     export declare type EventCallback = (event: Event) => void;
303
304     /**
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.
307      */
308     const eventListeners: Map<EventName, Array<EventCallback>> = new Map<EventName, Array<EventCallback>>();
309     eventNamesList.forEach((event: EventName) => eventListeners.set(event, []));
310      
311     /**
312      * Device feature codes.
313      */
314     export enum DeviceFeature {
315         BusyLight = 1000,
316         FactoryReset = 1001,
317         PairingList = 1002,
318         RemoteMMI = 1003,
319         MusicEqualizer = 1004,
320         EarbudInterconnectionStatus = 1005,
321         StepRate = 1006,
322         HeartRate = 1007,
323         RRInterval = 1008,
324         RingtoneUpload = 1009,
325         ImageUpload = 1010,
326         NeedsExplicitRebootAfterOta = 1011,
327         NeedsToBePutIncCradleToCompleteFwu = 1012,
328         RemoteMMIv2 = 1013,
329         Logging = 1014,
330         PreferredSoftphoneListInDevice = 1015,
331         VoiceAssistant = 1016,
332         PlayRingtone=1017
333     };
334
335     /**
336      * A specification of a button for MMI capturing.
337      */
338     export enum RemoteMmiType {
339         MMI_TYPE_MFB       = 0,
340         MMI_TYPE_VOLUP     = 1,
341         MMI_TYPE_VOLDOWN   = 2,
342         MMI_TYPE_VCB       = 3,
343         MMI_TYPE_APP       = 4,
344         MMI_TYPE_TR_FORW   = 5,
345         MMI_TYPE_TR_BACK   = 6,
346         MMI_TYPE_PLAY      = 7,
347         MMI_TYPE_MUTE      = 8,
348         MMI_TYPE_HOOK_OFF  = 9,
349         MMI_TYPE_HOOK_ON   = 10,
350         MMI_TYPE_BLUETOOTH = 11,
351         MMI_TYPE_JABRA     = 12,
352         MMI_TYPE_BATTERY   = 13,
353         MMI_TYPE_PROG      = 14,
354         MMI_TYPE_LINK      = 15,
355         MMI_TYPE_ANC       = 16,
356         MMI_TYPE_LISTEN_IN = 17,
357         MMI_TYPE_DOT3      = 18,
358         MMI_TYPE_DOT4      = 19,
359         MMI_TYPE_ALL       = 255
360     };
361
362     /**
363      * A MMI effect specification for light on, off or blinking in different tempo.
364      */
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
370     };
371
372     /**
373      * MMI button actions reported when button has focus.
374      */
375     export enum RemoteMmiActionInput {
376         MMI_ACTION_UP            = 1,
377         MMI_ACTION_DOWN          = 2,
378         MMI_ACTION_TAP           = 4,
379         MMI_ACTION_DOUBLE_TAP    = 8,
380         MMI_ACTION_PRESS         = 16,
381         MMI_ACTION_LONG_PRESS    = 32,
382         MMI_ACTION_X_LONG_PRESS  = 64
383     }; 
384
385     /**
386      * A 3 x 8 bit set of RGB colors. Numbers can be between 0-255.
387      */
388     export type ColorType = [number, number, number];
389
390     /**
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.
394      */
395     export let logLevel: number = 2;
396
397     /**
398      * An internal logger helper.
399      */
400     const logger = new class {
401         trace(msg: string) {
402             if (logLevel >= 4) {
403                 console.log(msg);
404             }
405         };
406
407         info(msg: string) {
408             if (logLevel >= 3) {
409                 console.log(msg);
410             }
411         };
412
413         warn(msg: string) {
414             if (logLevel >= 2) {
415                 console.warn(msg);
416             }
417         };
418
419         error(msg: string) {
420             if (logLevel >= 1) {
421                 console.error(msg);
422             }
423         };
424     };
425
426     /**
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.
429      */
430     const apiClientId: string = Math.random().toString(36).substr(2, 9);
431
432     /**
433      * A mapping from unique request ids for commands and the promise information needed 
434      * to resolve/reject them by an incomming event.
435      */
436     const sendRequestResultMap: Map<string, PromiseCallbacks> = new Map<string, PromiseCallbacks>();
437
438     /**
439     * A counter used to generate unique request ID's used to match commands and returning events.
440     */
441     let requestNumber: number = 1;
442
443     /**
444      * Contains initialization information used by the init/shutdown methods.
445      */
446     let initState: {
447         initialized?: boolean;
448         initializing?: boolean;
449         eventCallback?: (event: any) => void;
450     } = {};
451
452     /**
453      * The JavaScript library must be initialized using this function. It returns a promise that
454      * resolves when initialization is complete.
455     */
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);
460             if (!isChrome) {
461                 return reject(new Error("Jabra Browser Integration: Only supported by <a href='https://google.com/chrome'>Google Chrome</a>."));
462             }
463
464             if (initState.initialized || initState.initializing) {
465                 return reject(new Error("Jabra Browser Integration already initialized"));
466             }
467
468             initState.initializing = true;
469             sendRequestResultMap.clear();
470             let duringInit = true;
471
472             initState.eventCallback = (event: any) => {
473                 if (event.source === window &&
474                     event.data.direction &&
475                     event.data.direction === "jabra-headset-extension-from-content-script") {
476
477                     let eventApiClientId = event.data.apiClientId || "";
478                     let requestId = event.data.requestId || "";
479
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));
483
484                         // For backwards compatibility a blank message might be send as "na".
485                         if (event.data.message === "na") {
486                             delete event.data.message;
487                         }
488
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
493                         }
494
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;
498
499                             if (normalizedMsg.startsWith("logLevel")) {
500                                 logLevel = parseInt(event.data.message.substring(16));
501                                 logger.trace("Logger set to level " + logLevel);
502
503                                 // Loglevels are internal events and not an indication of proper
504                                 // initialization so skip rest of handling for log levels.
505                                 return;
506                             }
507
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 = {};
515                                     }
516                                     event.data.data.version_jsapi = apiVersion;
517                                 }
518
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);
522                                 }
523                              
524                                 // Lookup and check that we have identified a (real) command target to pair result with.
525                                 let resultTarget = identifyAndCleanupResultTarget(requestId);
526                                 if (resultTarget) {
527                                     let result: any;
528                                     if (event.data.data) {
529                                         result = event.data.data;
530                                     } else {
531                                         let dataPosition = commandEventsList[commandIndex].length + 1;
532                                         let dataStr = normalizedMsg.substring(dataPosition);
533                                         result = {};
534                                         if (dataStr) {
535                                           result.legacy_result =  dataStr;
536                                         };
537                                     }
538     
539                                     resultTarget.resolve(result)
540                                 } else {
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";
542                                     logger.error(err);
543                                     notify("error", {
544                                         error: err,
545                                         message: event.data.message
546                                     });
547                                 }                                
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;
554
555                                 notify(normalizedMsg as EventName, clientEvent);
556                             } else {
557                                 logger.warn("Unknown message: " + event.data.message);
558                                 notify("error", {
559                                     error: "Unknown message: ",
560                                     message: event.data.message
561                                 });
562                                 // Don't let unknown messages complete initialization so stop here.
563                                 return;
564                             }
565
566                             if (duringInit) {
567                                 duringInit = false;
568                                 return resolve();
569                             }
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;
573
574                             // Reject target promise if there is one - otherwise send a general error.
575                             let resultTarget = identifyAndCleanupResultTarget(requestId);
576                             if (resultTarget) {
577                                 resultTarget.reject(new CommandError(resultTarget.cmd, normalizedError, event.data.data));
578                             } else {
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;
584
585                                 notify("error", clientError);
586                             }
587
588                             if (duringInit) {
589                                 duringInit = false;
590                                 return reject(new Error(event.data.error));
591                             }
592                         }
593                     }
594                 }
595             };
596
597             window.addEventListener("message", initState.eventCallback!);
598
599             // Initial getversion and loglevel.
600             setTimeout(
601                 () => {
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);
605
606                         sendCmd("logLevel", null, false);
607                     }).catch((error) => {
608                         logger.error(error);
609                     });
610                 },
611                 1000
612             );
613
614             // Check if the web-extension is installed
615             setTimeout(
616                 function () {
617                     if (duringInit === true) {
618                         duringInit = false;
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"));
621                     }
622                 },
623                 5000
624             );
625
626             /**
627              * Helper that checks if the installation is consistent.
628              */
629             function isInstallationOk(installInfo: InstallInfo): boolean {
630                 let browserSdkVersions = [installInfo.version_browserextension, installInfo.version_chromehost, installInfo.version_jsapi];
631   
632                 // Check that we have install information for all components.
633                 if (browserSdkVersions.some(v => !v) || !installInfo.version_nativesdk) {
634                     return false;
635                 }
636
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);
642                     } else {
643                         return undefined;
644                     }
645                 }).filter(v => v).every((v, i, arr) => v === arr[0])) {
646                     return false;
647                 }
648
649                 return true;
650             }
651
652             /**
653              * Post event/error to subscribers.
654              */
655             function notify(eventName: EventName, eventMsg: ClientMessage | ClientError): void {
656                 let callbacks = eventListeners.get(eventName);
657                 if (callbacks) {
658                     callbacks.forEach((callback) => {
659                         callback(eventMsg);
660                     });
661                 } else {
662                     // This should not occur unless internal event mappings in this file
663                     // are not configured correctly.
664                     logger.error("Unexpected unknown eventName: " + eventName);
665                 }
666             }
667
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. 
671             */
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;
676                 if (requestId) {
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]);
688                 } else {
689                     // No idea what target matches what request - give up.
690                     resultTarget = undefined;
691                 }
692
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 + ")");
697                 }
698
699                 return resultTarget;
700
701             }
702
703             initState.initialized = true;
704             initState.initializing = false;
705         });
706     };
707
708     /**
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.
711     */
712     export function shutdown(): Promise<void> {
713         if (initState.initialized) {
714             window.removeEventListener("message", initState.eventCallback!);
715             initState.eventCallback = undefined;
716             sendRequestResultMap.clear();
717             requestNumber = 1;
718             initState.initialized = false;
719
720             // Unsubscribe all.
721             eventListeners.forEach((value, key) => {
722                 value = [];
723             });
724             return Promise.resolve();
725         }
726
727         return Promise.reject(new Error("Browser integration not initialized"));
728     };
729
730     /**
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.
733      */
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))
740         } else { // String
741             if (eventListeners.has(nameSpec as EventName)) {
742              return [ nameSpec ];
743             } else {
744                 logger.warn("Unknown event " + nameSpec + " ignored when adding/removing eventlistener");
745             }
746         }
747
748         return [];
749     }
750
751     /**
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.
756      */
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);
762             }
763         });
764     };
765
766     /**
767      * Remove existing listener to specified event(s). The callback must correspond to the exact callback provided
768      * to a previous addEventListener. 
769      */
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);
776             }
777         });
778     };
779
780     /**
781     * Activate ringer (if supported) on the Jabra Device
782     */
783     export function ring(): void {
784         sendCmd("ring");
785     };
786
787     /**
788     * Change state to in-a-call.
789     */
790     export function offHook(): void {
791         sendCmd("offhook");
792     };
793
794     /**
795     * Change state to idle (not-in-a-call).
796     */
797     export function onHook(): void {
798         sendCmd("onhook");
799     };
800
801     /**
802     * Mutes the microphone (if supported).
803     */
804     export function mute(): void {
805         sendCmd("mute");
806     };
807
808     /**
809     * Unmutes the microphone (if supported).
810     */
811     export function unmute(): void {
812         sendCmd("unmute");
813     };
814
815     /**
816     * Change state to held (if supported).
817     */
818     export function hold(): void {
819         sendCmd("hold");
820     };
821
822     /**
823     * Change state from held to OffHook (if supported).
824     */
825     export function resume(): void {
826         sendCmd("resume");
827     };
828
829     /**
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.
832     * 
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.
835     * 
836     * @returns A promise that is resolved once operation completes.
837     */
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", { 
842             type: typeVal,
843             capture: captureVal
844         });
845     }
846
847     /**
848     * Change light/color on a previously captured button.
849     * Nb. This requires the button to be previously captured though setMMiFocus.
850     * 
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.
854     * 
855     * @returns A promise that is resolved once operation completes.
856     */
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", { 
862             type: typeVal,
863             color: colorVal,
864             effect: effectVal
865         });        
866     }
867
868     /**
869     * Internal helper to get detailed information about the current active Jabra Device
870     * from SDK, including current status but excluding media device information.
871     */
872     function _doGetActiveSDKDevice(): Promise<DeviceInfo> {
873       return sendCmdWithResult<DeviceInfo>("getactivedevice");
874     };
875
876     /**
877     * Internal helper to get detailed information about the all attached Jabra Devices
878     * from SDK, including current status but excluding media device information.
879     */
880     function _doGetSDKDevices(): Promise<ReadonlyArray<DeviceInfo>> {
881         return sendCmdWithResult<ReadonlyArray<DeviceInfo>>("getdevices");
882     };
883
884     /**
885     * Get detailed information about the current active Jabra Device, including current status
886     * and optionally also including related browser media device information. 
887     * 
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.
892     */
893     export function getActiveDevice(includeBrowserMediaDeviceInfo: boolean | string = false): Promise<DeviceInfo> {
894         let includeBrowserMediaDeviceInfoVal = booleanOrString(includeBrowserMediaDeviceInfo);
895         if (includeBrowserMediaDeviceInfoVal) {
896             return _doGetActiveSDKDevice_And_BrowserDevice();
897         } else {
898             return _doGetActiveSDKDevice();
899         }
900     };
901
902     /**
903     * List detailed information about all attached Jabra Devices, including current status.
904     * and optionally also including related browser media device information.
905     * 
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.
910     */
911     export function getDevices(includeBrowserMediaDeviceInfo: boolean | string = false): Promise<ReadonlyArray<DeviceInfo>> {
912         let includeBrowserMediaDeviceInfoVal = booleanOrString(includeBrowserMediaDeviceInfo);
913         if (includeBrowserMediaDeviceInfoVal) {
914             return _doGetSDKDevices_And_BrowserDevice();
915         } else {
916             return _doGetSDKDevices();
917         }
918      };
919
920     /**
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.
923     * 
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.
927     * 
928     * @deprecated Use setActiveDeviceId instead.
929     */
930     export function _setActiveDeviceId(id: number | string): void {
931         let idVal =  numberOrString(id);
932         
933         // Use both new and old way of passing parameters for compatibility with <= v0.5.
934         sendCmd("setactivedevice " + id.toString(), { id: idVal } );
935     };
936     
937     /**
938     * Select a new active device returning once selection is completed.
939     * 
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.
943     * 
944     * @param id The id number of the new active device.
945     * @returns A promise that is resolved once selection completes.
946     * 
947     */
948     export function setActiveDeviceId(id: number | string): Promise<void> {
949       let idVal =  numberOrString(id);
950     
951       return sendCmdWithResult<void>("setactivedevice2", { id: idVal } );
952     };
953
954     /**
955     * Set busylight on active device (if supported)
956     * 
957     * @param busy True if busy light should be set, false if it should be cleared.
958     */
959     export function setBusyLight(busy: boolean | string): Promise<void> {
960         let busyVal = booleanOrString(busy);
961        
962         return sendCmdWithResult<void>("setbusylight", { busy: busyVal } );
963     };
964
965     /**
966     * Get version number information for all components.
967     */
968     export function getInstallInfo(): Promise<InstallInfo> {
969         return sendCmdWithResult<InstallInfo>("getinstallinfo");
970     };
971
972     /**
973     * Internal helper that forwards a command to the browser extension
974     * without expecting a response.
975     */
976     function sendCmd(cmd: string, args: object | null = null, requireInitializedCheck: boolean = true): void {
977         if (!requireInitializedCheck || (requireInitializedCheck && initState.initialized)) {
978             let requestId = (requestNumber++).toString();
979
980             let msg = {
981                 direction: "jabra-headset-extension-from-page-script",
982                 message: cmd,
983                 args: args || {},
984                 requestId: requestId,
985                 apiClientId: apiClientId,
986                 version_jsapi: apiVersion
987             };
988
989             logger.trace("Sending command to content script: " + JSON.stringify(msg));
990
991             window.postMessage(msg, "*");
992         } else {
993             throw new Error("Browser integration not initialized");
994         }
995     };
996
997     /**
998     * Internal helper that forwards a command to the browser extension
999     * expecting a response (a promise).
1000     */
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();
1004
1005             return new Promise<T>((resolve, reject) => {
1006                 sendRequestResultMap.set(requestId, { cmd, resolve, reject });
1007
1008                 let msg = {
1009                     direction: "jabra-headset-extension-from-page-script",
1010                     message: cmd,
1011                     args: args || {},
1012                     requestId: requestId,
1013                     apiClientId: apiClientId,
1014                     version_jsapi: apiVersion
1015                 };
1016
1017                 logger.trace("Sending command to content script expecting result: " + JSON.stringify(msg));
1018
1019                 window.postMessage(msg, "*");
1020             });
1021         } else {
1022             return Promise.reject(new Error("Browser integration not initialized"));
1023         }
1024     };
1025
1026     /**
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.
1029     */
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'));
1033         }
1034
1035         if (!(typeof ((audioElement as any).setSinkId) === "function")) {
1036             return Promise.reject(new Error('Your browser does not support required Audio Output Devices API'));
1037         }
1038
1039         return (audioElement as any).setSinkId(deviceInfo.browserAudioOutputId).then(() => {
1040             var success = (audioElement as any).sinkId === deviceInfo.browserAudioOutputId;
1041             return success;
1042         });
1043     };
1044
1045     /**
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.
1048      */
1049     export function isDeviceSelectedForInput(mediaStream: MediaStream, deviceInfo: DeviceInfo): boolean {
1050         if (!mediaStream || !deviceInfo) {
1051             throw Error('Call to isDeviceSelectedForInput has argument(s) missing');
1052         }
1053
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) {
1059                 return false;
1060             }
1061         }
1062
1063         return true;
1064     };
1065
1066     /**
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.
1070     * 
1071     * Optional, additional non-audio constrains (like f.x. video) can be specified as well.
1072     * 
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.
1075     */
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'));
1080         }
1081
1082         // Init completed ?
1083         if (!initState.initialized) {
1084             return Promise.reject(new Error("Browser integration not initialized"));
1085         }
1086
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)");
1090         }
1091
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"));
1095         }
1096
1097         /**
1098          * Utility method that combines constraints with ours taking precedence (deep). 
1099          */
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.
1105                 return result;
1106             } else {
1107                 return ours;
1108             }
1109         }
1110
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) => {
1118                     track.stop();
1119                 });
1120
1121                 if (deviceInfo && deviceInfo.browserAudioInputId) {                   
1122                     return navigator.mediaDevices.getUserMedia(mergeConstraints({ audio: { deviceId: deviceInfo.browserAudioInputId } }, constraints))
1123                         .then((stream) => {
1124                             return {
1125                                 stream: stream,
1126                                 deviceInfo: deviceInfo
1127                             };
1128                         })
1129                 } else {
1130                     return Promise.reject(new Error('Could not find a Jabra device with a microphone'));
1131                 }
1132             })
1133         });
1134     };
1135
1136     /**
1137      * Internal helper for add media information properties to existing SDK device information.
1138      */
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();
1146                 
1147                 var costs = new Array();
1148                 for (var i = 0; i <= s1.length; i++) {
1149                     var lastValue = i;
1150                     for (var j = 0; j <= s2.length; j++) {
1151                     if (i == 0)
1152                         costs[j] = j;
1153                     else {
1154                         if (j > 0) {
1155                         var newValue = costs[j - 1];
1156                         if (s1.charAt(i - 1) != s2.charAt(j - 1))
1157                             newValue = Math.min(Math.min(newValue, lastValue),
1158                             costs[j]) + 1;
1159                         costs[j - 1] = lastValue;
1160                         lastValue = newValue;
1161                         }
1162                     }
1163                     }
1164                     if (i > 0)
1165                     costs[s2.length] = lastValue;
1166                 }
1167                 return costs[s2.length];
1168             }
1169             
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 {
1173                 let longer = s1;
1174                 let shorter = s2;
1175                 if (s1.length < s2.length) {
1176                     longer = s2;
1177                     shorter = s1;
1178                 }
1179                 let longerLength = longer.length;
1180                 if (longerLength === 0) {
1181                     return 1.0;
1182                 }
1183                 return (longerLength - editDistance(longer, shorter)) / longerLength;
1184             }
1185
1186             if (mediaDeviceNameCandidates.length == 1) {
1187                 return 0;
1188             } else if (mediaDeviceNameCandidates.length > 0) {
1189                 let similarities = mediaDeviceNameCandidates.map(candidate => {
1190                     if (candidate.includes("(" + sdkDeviceName + ")")) {
1191                         return 1.0;
1192                     } else {
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;
1196
1197                         return levenshteinDistance(sdkDeviceName, cleanedCandidate)
1198                     }
1199                 });
1200                 let bestMatchIndex = similarities.reduce((prevIndexMax, value, i, a) => value > a[prevIndexMax] ? i : prevIndexMax, 0);
1201                 return bestMatchIndex;
1202             } else {
1203                 return -1;
1204             }
1205         }
1206             
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);
1210         }
1211         
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);
1230                     if (outputDevice) {
1231                         audioOutputId = outputDevice.deviceId;
1232                     }
1233                 }
1234                 else if (foundDevice.kind === 'audiooutput') {
1235                     audioOutputId = foundDevice.deviceId;
1236                     // Lookup matching output input device:
1237                     let inputDevice = findMatchingMediaDevice(groupId, 'audioinput', jabraMediaDevices);
1238                     if (inputDevice) {
1239                         audioInputId = inputDevice.deviceId;
1240                     }
1241                 }
1242             }
1243             if (groupId) {
1244                 deviceInfo.browserGroupId = groupId;
1245             }
1246             if (label) {
1247                 deviceInfo.browserLabel = label;
1248             }
1249             if (audioInputId) {
1250                 deviceInfo.browserAudioInputId = audioInputId;
1251             }
1252             if (audioOutputId) {
1253                 deviceInfo.browserAudioOutputId = audioOutputId;
1254             }
1255         } else {
1256             // Do nothing if device information is missing.
1257         }
1258     }
1259
1260     /** 
1261      * Internal helper that returns complete device information, including both SDK and browser media device 
1262      * information for all devices. 
1263      * 
1264      * Chrome note:
1265      * 1) Only works if hosted under https.
1266      * 
1267      * Firefox note:
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.
1271      * 
1272      * General non-chrome browser note:  
1273      * 1) Returning output devices requires support for new Audio Output Devices API.
1274      */
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'));
1279         }
1280
1281         // Init completed ?
1282         if (!initState.initialized) {
1283             return Promise.reject(new Error("Browser integration not initialized"));
1284         }
1285
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'));
1289         }
1290
1291         return Promise.all([_doGetSDKDevices(), navigator.mediaDevices.enumerateDevices()]).then( ([deviceInfos, mediaDevices]) => {
1292             deviceInfos.forEach( (deviceInfo) => {
1293                 fillInMatchingMediaInfo(deviceInfo, mediaDevices);
1294             });
1295
1296             return deviceInfos;
1297         });
1298     }
1299
1300     /** 
1301      * Internal helper that returns complete device information, including both SDK and browser media device 
1302      * information for active device. 
1303      * 
1304      * Chrome note:
1305      * 1) Only works if hosted under https.
1306      * 
1307      * Firefox note:
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.
1311      * 
1312      * General non-chrome browser note:  
1313      * 1) Returning output devices requires support for new Audio Output Devices API.
1314      */
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'));
1319         }
1320
1321         // Init completed ?
1322         if (!initState.initialized) {
1323             return Promise.reject(new Error("Browser integration not initialized"));
1324         }
1325
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'));
1329         }
1330
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);
1334             return deviceInfo;
1335         });
1336     };
1337
1338      /**
1339      * Helper that pass boolean values through and parses strings to booleans.
1340      */
1341     function booleanOrString(arg: boolean | string) : boolean
1342     {
1343         if (arg !== "" && ((typeof arg === 'string') || ((arg as any) instanceof String))) {
1344             return (arg === 'true' || arg === '1');
1345         } else if (typeof(arg) === "boolean")  {
1346             return arg;
1347         } else {
1348             throw new Error("Illegal/missing argument - boolean or string expected");
1349         }
1350     }
1351
1352     /**
1353      * Helper that pass numbers through and parses strings to numbers.
1354      */
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') {
1359             return arg;
1360         } else {
1361             throw new Error("Illegal/missing argument - number or string expected");
1362         }
1363     };
1364
1365     /**
1366      * Helper that pass color array through and converts values to color array.
1367      */
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)");
1378             }
1379             return arg;
1380         } else {
1381             throw new Error("Illegal/missing argument - number array or hex string expected");
1382         }
1383     };
1384 };