1 /** 2 * @fileOverview cwic.js 3 * CWIC is a jQuery plugin to access the Cisco Web Communicator<br> 4 * Audio and Video media require the CWC browser plugin to be installed <br> 5 */ 6 7 /* 8 CWIC is a jQuery plugin to access the Cisco Web Communicator 9 10 CWIC uses jQuery features such as:<ul> 11 <li>'cwic' namespacing: jQuery.cwic();</li> 12 <li>custom events (.cwic namespace): conversationStart.cwic</li> 13 <li>attach data with the 'cwic' key: ui.data('cwic', conversation)</li> 14 </ul> 15 16 Audio and Video media require the CWC browser plugin to be installed 17 18 */ 19 /** 20 * Global window namespace 21 * @name window 22 * @namespace 23 */ 24 /*global jQuery window ActiveXObject _triggerError _reset _registerSystemCallbacks _registerCallChangeCallbacks _triggerConversationEvent unregisterPhone shutdown _log*/ 25 (function( $ ) 26 /** @scope $.fn.cwic */ 27 { 28 29 // a global reference to the CWC native plugin API 30 var _plugin = null; 31 /** cwic global settings, they can be overridden by passing options to init 32 * @namespace 33 */ 34 var settings = { 35 /** A function to implement base64 encoding required for HTTP Basic authentication against the {@link $.fn.cwic-settings.node} service.<br> 36 * This function is required if attempting authentication against the node service using some versions of Internet Explorer.<br> 37 * If not provided cwic will attempt to use window.btoa which is not available on all browsers. 38 * @type Function=null 39 * @function 40 * @param {String} Input buffer to encode. 41 * @return {String} Base64 encoded string. 42 */ 43 encodeBase64: null, 44 /** The handler to be called when the API is ready.<br> 45 * The values in the defaults parameter can be used when invoking registerPhone 46 * The API is ready when:<ul> 47 * <li>The document (DOM) is ready.</li> 48 * <li>The CWC plugin was found and could be loaded.</li></ul> 49 * @type Function=null 50 * @param {Object} defaults An object containing default values retrieved from URL query parameters user and/or cucm <i>e.g: http://myserver/phone?user=foo&cucm=1.2.3.4 </i><br> 51 * @param {Boolean} registered Phone registration status - true if the phone is already registered (can be used when using SDK in multiple browser tabs), false otherwise 52 * @param {String} mode The phone's current call control mode - "SoftPhone" or "DeskPhone" 53 */ 54 ready: null, 55 /** use ccmcip when authenticating 56 * @type Boolean=true 57 */ 58 useCcmcip: true, 59 /** device prefix to use for default device prediction algorithm 60 * @type String='ecp' 61 */ 62 devicePrefix: 'ecp', 63 /** callback function to predict softphone device name<br> 64 * default implementation is to concatenate settings.devicePrefix + options.username 65 * @name $.fn.cwic-settings.predictDevice 66 * @type Function=null 67 * @function 68 * @param {Object} options 69 * @param {String} options.username 70 */ 71 /** A flag to indicate to cwic that it should log more messages. 72 * @type Boolean=false 73 */ 74 verbose: false, 75 /** Handler to be called when cwic needs to log information.<br> 76 * Default is to use console.log if available, otherwise do nothing. 77 * @function 78 * @param {String} msg the message 79 * @param {Object} [context] the context of the message 80 * @type Function 81 */ 82 log: function(/** String */msg, /** Object */context) { 83 if (typeof console !== "undefined" && console.log) { 84 console.log(msg); 85 if (context) { 86 console.log(context); 87 } 88 } 89 }, 90 /** The handler to be called if the API could not be initialized.<br> 91 * The basic properties of the error object are listed, however more may be added based on the context of the error.<br> 92 * If the triggered error originated from a caught exception, the original error properties are included in the error parameter.<br> 93 * An error with code 1 (PluginNotAvailable) can have an extra 'pluginDisabled' property set to true.<br> 94 * @type Function 95 * @param {Object} error see {@link $.fn.cwic-errorMap} 96 * @param {String} [error.message] message associated with the error. 97 * @param {Number} error.code code associated with the error 98 */ 99 error: function(/** Object */error) { 100 _log("Error: ",error); 101 }, 102 /** The node address: host name or IP address.<br> 103 * cwic can use some network services hosted on a web server (the node). By default, this server is the host that serves the web application based on cwic.<br> 104 * The node deployment is optional. It provides advanced features such as directory integration. See additional documentation in the 'node' folder. <br> 105 * @type String='' 106 */ 107 node: '', // to access node.cwc services, can be cross-domain (JSONP) 108 /** 109 * Allows the application to extend the default error map.<br> 110 * This parameter is a map of error id to {@link $.fn.cwic-errorMapEntry} 111 * It may also be a map of error id to String 112 * By default error messages (String) are associated to error codes (map keys, Numbers).<br> 113 * The application can define new error codes, or associate a different message/object to a pre-defined error code. <br> 114 * Default error map: {@link $.fn.cwic-errorMap}<br> 115 * @name $.fn.cwic-settings.errorMap 116 * @type $.fn.cwic-errorMapEntry{} 117 */ 118 /** 119 * The default "move audio port on video call" policy 120 * @type Boolean=false 121 */ 122 mediaPortSplitting: false 123 }; 124 125 // jsdoc does not seem to like enumerating properties (fields) of objects it already considers as properties (fields). 126 /** cwic error object 127 * @name $.fn.cwic-errorMapEntry 128 * @namespace 129 * @property {Number} code a unique error code 130 * @property {String} message the message associated with the error 131 * @property {Any} [propertyName] Additional properties that will be passed back when an error is raised. 132 */ 133 /** 134 * The error map used to build errors triggered by cwic. <br> 135 * Keys are error codes (numbers), values objects associated to codes. <br> 136 * By default the error object contains a single 'message' property. <br> 137 * The error map can be customized via the init function. <br> 138 * @namespace 139 */ 140 var errorMap = { 141 /** 0: unknown error or exception 142 * @type $.fn.cwic-errorMapEntry 143 */ 144 Unknown : { code: 0, message: "Unknown error" }, 145 /** 1: plugin not available (not installed, not enabled or unable to load) 146 * @type $.fn.cwic-errorMapEntry 147 */ 148 PluginNotAvailable : { code: 1, message: "Plugin not available" }, 149 /** 2: browser not supported 150 * @type $.fn.cwic-errorMapEntry 151 */ 152 BrowserNotSupported : { code: 2, message: "Browser not supported" }, 153 /** 3: invalid arguments 154 * @type $.fn.cwic-errorMapEntry 155 */ 156 InvalidArguments : { code: 3, message: "Invalid arguments" }, 157 /** 4: invalid state for operation (e.g. startconversation when phone is not registered) 158 * @type $.fn.cwic-errorMapEntry 159 */ 160 InvalidState : { code: 4, message: "Invalid State" }, 161 /** 5: plugin returned an error 162 * @type $.fn.cwic-errorMapEntry 163 */ 164 NativePluginError: { code: 5, message: "Native plugin error" }, 165 /** 6: operation not supported 166 * @type $.fn.cwic-errorMapEntry 167 */ 168 OperationNotSupported: { code: 6, message: "Operation not supported" }, 169 /** 7: no call manager specified 170 * @type $.fn.cwic-errorMapEntry 171 */ 172 ReleaseMismatch : { code: 7, message: "Release mismatch" }, 173 /** 10: release mismatch 174 * @type $.fn.cwic-errorMapEntry 175 */ 176 NoCallManagerConfigured : { code: 10, message: "No CUCM found" }, 177 /** 11: no devices found for supplied credentials 178 * @type $.fn.cwic-errorMapEntry 179 */ 180 NoDevicesFound : { code: 11, message: "No devices found" }, 181 /** 12: no softphone (CSF) devices found for supplied credentials 182 * @type $.fn.cwic-errorMapEntry 183 */ 184 NoCsfDevicesFound : { code: 12, message: "No CSF device found" }, 185 /** 13: other phone configuration error 186 * @type $.fn.cwic-errorMapEntry 187 */ 188 PhoneConfigGenError : { code: 13, message: "Phone configuration error" }, 189 /** 14: SIP profile error 190 * @type $.fn.cwic-errorMapEntry 191 */ 192 SipProfileGenError : { code: 14, message: "Sip profile error" }, 193 /** 15: configuration not set e.g. missing TFTP/CTI/CCMCIP addresses 194 * @type $.fn.cwic-errorMapEntry 195 */ 196 ConfigNotSet : { code: 15, message: "Configuration not set" }, 197 /** 16: could not fetch phone configuration 198 * @type $.fn.cwic-errorMapEntry 199 */ 200 TftpFetchError : { code: 16, message: "TFTP fetch error" }, 201 /** 18: already logged in in another process (browser or window in internet explorer) 202 * @type $.fn.cwic-errorMapEntry 203 */ 204 LoggedInElseWhere : { code: 18, message: "Already logged in" }, 205 /** 19: authentication failed - invalid username or password for configured server 206 * @type $.fn.cwic-errorMapEntry 207 */ 208 AuthenticationFailure : { code: 19, message: "Authentication failed" }, 209 /** 20: other login error 210 * @type $.fn.cwic-errorMapEntry 211 */ 212 LoginError: { code: 20, message: "Login Error"}, 213 /** 21: no username and/or password supplied 214 * @type $.fn.cwic-errorMapEntry 215 */ 216 NoCredentialsConfigured: { code: 21, message: "No credentials configured"}, 217 /** 30: error performing call control operation 218 * @type $.fn.cwic-errorMapEntry 219 */ 220 CallControlError: { code: 30, message: "Call control error"}, 221 /** 40: error modifying video association (e.g. removing non-attached window or adding non-existing window) 222 * @type $.fn.cwic-errorMapEntry 223 */ 224 VideoWindowError: { code: 40, message: "Video window error"}, 225 /** 999: no error (success) 226 * @type $.fn.cwic-errorMapEntry 227 */ 228 NoError: { code: 999, message: "No Error"} 229 }; 230 231 var errorMapAlias = { 232 // ApiReturnCodeEnum 233 Ok: 'NoError', 234 eNoError: 'NoError', 235 eInvalidCallId: 'InvalidArguments', 236 eCreateCallFailed: 'CallControlError', 237 eNoActiveDevice: 'PhoneConfigGenError', 238 eTftpNotConfigured: 'ConfigNotSet', 239 eCallOperationFailed: 'CallControlError', 240 eLoggedInLock: 'LoggedInElseWhere', 241 eLogoutFailed: 'LoginError', 242 eCcmcipNotConfigured: 'ConfigNotSet', 243 eWindowAlreadyExists: 'VideoWindowError', 244 eInvalidState: 'InvalidState', 245 eCtiNotConfigured: 'ConfigNotSet', 246 eNoPhoneMode: 'InvalidArgument', 247 eNoWindowExists: 'VideoWindowError', 248 eInvalidArgument: 'InvalidArguments', 249 // ConnectionFailureCodeEnum 250 //eNoError, 251 eUnknownFailure: 'LoginError', 252 //eInvalidState, 253 eUnsupportedPhoneMode: 'LoginError', 254 eNoAuthServersConfigured: 'NoCallManagerConfigured', 255 eNoCredentialsConfigured: 'NoCredentialsConfigured', 256 eAuthCouldNotConnect: 'LoginError', 257 eAuthServerCertificateRejected: 'LoginError', 258 eCredentialsRejected: 'LoginError', 259 eAuthResponseEmpty: 'LoginError', 260 eAuthResponseInvalid: 'LoginError', 261 eNoTftpServersConfigured: 'NoCallManagerConfigured', 262 eNoDeviceNameConfigured: 'LoginError', 263 eLineMustNotBeConfigured: 'LoginError', 264 eNoLocalIpConfigured: 'LoginError', 265 eTftpCouldNotConnect: 'TftpFetchError', 266 eTftpFileNotFound: 'TftpFetchError', 267 eTftpFileEmpty: 'TftpFetchError', 268 eTftpFileInvalid: 'TftpFetchError', 269 eRequireAuthenticationString: 'LoginError', 270 eCapfEnrolmentFailed: 'LoginError', 271 eRequireSecureCachePath: 'LoginError', 272 eRequireSecurityLibrary: 'LoginError', 273 eStorageError: 'LoginError', 274 eSecurityLibraryError: 'LoginError', 275 eCapfEnrolmentRequired: 'LoginError', 276 eNoCtiServersConfigured: 'LoginError', 277 eDeviceNotInService: 'LoginError', 278 // AuthenticationFailureCode 279 //eNoError: '', 280 eNoServersConfigured: 'NoCallManagerConfigured', 281 //eNoCredentialsConfigured: 'NoCredentialsConfigured', 282 eCouldNotConnect: 'LoginError', 283 eServerCertificateRejected: 'LoginError', 284 //eCredentialsRejected: 'LoginError', 285 eResponseEmpty: 'LoginError', 286 eResponseInvalid: 'LoginError', 287 eOperationNotSupported: 'OperationNotSupported' 288 289 }; 290 var getError = function(key,backupkey) { 291 var errorMapKey = 'Unknown'; 292 if(errorMapAlias[key]) { 293 errorMapKey = errorMapAlias[key]; 294 } else if(errorMap[key]) { 295 errorMapKey = key; 296 } else if(backupkey && errorMapAlias[backupkey]) { 297 errorMapKey = errorMapAlias[backupkey]; 298 } else if(backupkey && errorMap[backupkey]) { 299 errorMapKey = backupkey; 300 } 301 return errorMap[errorMapKey]; 302 }; 303 /** 304 * Registration object with properties of the currently logged in session <br> 305 * expanded below in _getRegistrationObject() and authenticateCcmcip() <br> 306 * @type Object 307 * @private 308 */ 309 var registration = { 310 devices: {} // map of available devices (key is device name) 311 }; 312 313 var registering = { 314 registeringPhone : false, 315 switchingMode: false, 316 successCb : null, 317 errorCb : null, 318 CUCM : [], 319 password : '', 320 unregisterCb : null 321 }; 322 323 var videowindows = {}; 324 /** 325 * an internal function to log messages 326 * @param (Boolean) [isVerbose] indicates if msg should be logged in verbose mode only (configurable by the application) <br> 327 * @param (String) msg the message to be logged (to console.log by default, configurable by the application) <br> 328 * @param (Object) [context] a context to be logged <br> 329 */ 330 function _log() { 331 var isVerbose = typeof arguments[0] === "boolean" ? arguments[0] : false; 332 var msg = typeof arguments[0] === "string" ? arguments[0] : arguments[1]; 333 var context = typeof arguments[1] === "object" ? arguments[1] : arguments[2]; 334 335 if ((!isVerbose || (isVerbose && settings.verbose)) && $.isFunction(settings.log)) { 336 try { 337 settings.log('[cwic] ' + msg, context); 338 } catch(e) { 339 // Exceptions in application-define log functions can't really be logged 340 } 341 } 342 } 343 344 // Helper function to check if plugin is still available. 345 // Related to DE3975. The CK advanced editor causes the overflow CSS attribute to change, which in turn 346 // removes and replaces the plugin during the reflow losing all state. 347 var _doesPluginExist = function() { 348 var ret = true; 349 try { 350 var fnref = _plugin.api.getCall; 351 } catch(e) { 352 ret = false; 353 } 354 return ret; 355 }; 356 357 // support unit tests for IE ($.browser.jasmine means we're running unit tests). 358 // should be more transparent than this, but tell that to internet explorer - you can't override attachEvent... 359 var _addListener = function(obj,type,handler) { 360 try { 361 if($.browser.jasmine) { 362 obj._addListener(type,handler,false); 363 } else if(document.attachEvent) { 364 obj.attachEvent("on" + type, handler); 365 } else { 366 obj.addEventListener(type, handler, false); 367 } 368 } 369 catch(e) { 370 } 371 }; 372 var _removeListener = function(obj,type,handler) { 373 try { 374 if($.browser.jasmine) { 375 return; 376 //obj._removeListener(type,handler,false); 377 } else if(document.attachEvent) { 378 obj.detachEvent("on" + type, handler); 379 } else { 380 obj.removeEventListener(type, handler, false); 381 } 382 } catch(e) { 383 } 384 }; 385 386 var rebootIfBroken = function(callback) { 387 var pluginExists = _doesPluginExist(); 388 if(!pluginExists) { 389 _log("Plugin has been unloaded. Restarting...."); 390 _plugin = null; 391 callback(); 392 } 393 return pluginExists; 394 }; 395 396 /** 397 * predict a device based on username 398 * @param {Object} options 399 * @type {String} 400 */ 401 function _predictDevice(options) { 402 if($.isFunction(settings.predictDevice)) { 403 try { 404 return settings.predictDevice(options); 405 } catch(predictDeviceException) { 406 _log('Exception occurred in application predictDevice callback',predictDeviceException); 407 if(typeof console !== "undefined" && console.trace) { 408 console.trace(); 409 } 410 } 411 412 } else { 413 return (options.username)? settings.devicePrefix+options.username : ''; 414 } 415 } 416 /** 417 * encode a string into base64 418 * @param {String} str 419 * @type {String} 420 */ 421 function _encodeBase64(str) { 422 // application encoding 423 if ($.isFunction(settings.encodeBase64)) { 424 try { 425 return settings.encodeBase64(str); 426 } catch(encodeBase64Exception) { 427 _log('Exception occurred in application encodeBase64 callback',encodeBase64Exception); 428 if(typeof console !== "undefined" && console.trace) { 429 console.trace(); 430 } 431 } 432 433 } 434 435 // btoa exists on Mozilla 436 if ($.isFunction(window.btoa)) { 437 return window.btoa(str); 438 } 439 440 // IE ? 441 _log('error: cannot encode base64'); 442 443 return ''; 444 } 445 446 /** 447 * manages asynchronous loading/resetting of windowhandles for local preview windows 448 * @private 449 */ 450 var previewwindows = { 451 // windows: map keyed by document->pluginId, value is: 452 // plugin: the plugin object (JSAPI/DOM) 453 // windowhandle: last known windowhandle 454 windows: [], 455 windowobjects: [], 456 getWindowId: function(args) { 457 var win = args.window; 458 var windowid = this.windowobjects.indexOf(win); 459 if(windowid === -1 && !args.readOnly) { 460 this.windowobjects.push(win); 461 windowid = this.windowobjects.indexOf(win); 462 } 463 return windowid; 464 }, 465 /** 466 * adds a video plugin as a preview window 467 * @param {Object} args 468 * @param {String|Object} args.plugin id of the video plugin or the plugin object itself 469 * @param {Object} args.window the parent window for this plugin (for popup support) 470 * @private 471 */ 472 add: function(args) { 473 if(!args) { 474 return; 475 } 476 if(!args.window) { 477 args.window = window; 478 } 479 var windowid = this.getWindowId({window:args.window}); 480 if(!this.windows[windowid]) { 481 this.windows[windowid]= {}; 482 } 483 if(typeof args.plugin === "string") { 484 this.windows[windowid][args.plugin] = { 485 windowhandle: null, 486 plugin: null, 487 used: false 488 }; 489 var elem = args.window.document.getElementById(args.plugin); 490 if(elem && elem.windowhandle) { 491 this.update({window: args.window, plugin: elem}); 492 } 493 } else if(args.plugin && args.plugin.id) { 494 this.windows[windowid][args.plugin.id] = { 495 windowhandle: args.plugin.windowhandle, 496 plugin: args.plugin, 497 used:false 498 }; 499 if(args.plugin.windowhandle && args.plugin.windowhandle !== "00000000") { 500 _plugin.api.addPreviewWindow({windowhandle: args.plugin.windowhandle}); 501 this.windows[windowid][args.plugin.id].used = true; 502 } 503 } 504 }, 505 /** 506 * updates all preview Windows or the specific one passed in 507 * @param {Object} [args] 508 * @param {Object} args.window the parent window for this plugin (for popup support) 509 * @param {Object} args.plugin 510 * @private 511 */ 512 update: function(args) { 513 var win; 514 // Add any queued preview windows if they've been loaded (i.e. have a windowhandle now) since being added 515 if(!args || !args.plugin || !args.plugin.id) { 516 for(win in this.windows) { 517 if(this.windows.hasOwnProperty(win)) { 518 for(var pluginId in this.windows[win]) { 519 if(this.windows[win].hasOwnProperty(pluginId)) { 520 if(!this.windows[win][pluginId].plugin) { 521 this.windows[win][pluginId].plugin = this.windowobjects[win].document.getElementById(pluginId); 522 } 523 if(this.windows[win][pluginId].plugin && this.windows[win][pluginId].plugin.windowhandle) { 524 if(this.windows[win][pluginId].used) { 525 if(this.windows[win][pluginId].windowhandle !== this.windows[win][pluginId].plugin.windowhandle) { 526 _plugin.api.removePreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle}); 527 this.windows[win][pluginId].windowhandle = this.windows[win][pluginId].plugin.windowhandle; 528 _plugin.api.addPreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle}); 529 } 530 } else { 531 this.windows[win][pluginId].windowhandle = this.windows[win][pluginId].plugin.windowhandle; 532 _plugin.api.addPreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle}); 533 this.windows[win][pluginId].used = true; 534 } 535 } 536 } 537 } 538 } 539 } 540 return; 541 } 542 // Check if the window handle has changed since last update 543 // If so, remove the old one, add the new one. 544 if(!args.window) { 545 args.window = window; 546 } 547 var windowid = this.getWindowId({window: args.window, readonly: true}); 548 if(windowid === -1 || !this.windows[windowid] || !this.windows[windowid][args.plugin.id]) { 549 return; 550 } 551 if(this.windows[windowid][args.plugin.id].windowhandle !== args.plugin.windowhandle) { 552 if(this.windows[windowid][args.plugin.id].windowhandle) { 553 _plugin.api.removePreviewWindow({windowhandle: this.windows[windowid][args.plugin.id].windowhandle}); 554 this.windows[windowid][args.plugin.id].used = false; 555 } 556 this.windows[windowid][args.plugin.id].windowhandle = args.plugin.windowhandle; 557 if(args.plugin.windowhandle) { 558 _plugin.api.addPreviewWindow({windowhandle: this.windows[windowid][args.plugin.id].windowhandle}); 559 this.windows[windowid][args.plugin.id].used = true; 560 } 561 } 562 }, 563 /** 564 * removes a plugin object from the list of preview windows 565 * @param {Object} args 566 * @param {String|Object} args.plugin 567 * @param {Object} args.window the parent window for this plugin (for popup support) 568 * @private 569 */ 570 remove: function(args) { 571 if(!args) { 572 return; 573 } 574 if(!args.window) { 575 args.window = window; 576 } 577 var windowid = this.getWindowId({window:args.window, readOnly: true}); 578 if(windowid === -1 || !this.windows[windowid]) { 579 return; 580 } 581 if(typeof args.plugin === "string" && windowid >=0 && this.windows[windowid] && this.windows[windowid][args.plugin]) { 582 if(this.windows[windowid][args.plugin].windowhandle) { 583 _plugin.api.removePreviewWindow({windowhandle: this.windows[windowid][args.plugin].windowhandle}); 584 } 585 delete this.windows[windowid][args.plugin]; 586 } 587 if(args.plugin && args.plugin.id) { 588 _plugin.api.removePreviewWindow({windowhandle: args.plugin.windowhandle}); 589 delete this.windows[windowid][args.plugin.id]; 590 } 591 }, 592 dump: function() { 593 _log("Dumping Preview Windows..."); 594 for(var win in this.windows) { 595 if(this.windows.hasOwnProperty(win)) { 596 _log(" window: "+this.windowobjects[win].document.location.pathname); 597 for(var pluginId in this.windows[win]) { 598 if(this.windows[win].hasOwnProperty(pluginId)) { 599 _log(" ID: " + pluginId + ", windowhandle: " + this.windows[win][pluginId].windowhandle + ", used: "+this.windows[win][pluginId].used); 600 } 601 } 602 } 603 } 604 } 605 }; 606 /** 607 * manages asynchronous loading/resetting of windowhandles for remote video objects related to calls 608 * and adds the window to the call if it's async loaded after the id has been added to the call 609 * 610 * @private 611 */ 612 var videowindowsbycall = { 613 // calls: map keyed by callId 614 // each element is a map keyed by window and pluginId, containing: 615 // plugin: the plugin object (JSAPI/DOM) 616 // windowhandle: the last known windowhandle added for this plugin id 617 calls: {}, 618 windowobjects: [], 619 getWindowId: function(args) { 620 var win = args.window; 621 var windowid = this.windowobjects.indexOf(win); 622 if(windowid === -1 && !args.readOnly) { 623 this.windowobjects.push(win); 624 windowid = this.windowobjects.indexOf(win); 625 } 626 return windowid; 627 }, 628 /** 629 * adds a video plugin to a call, adding the call to the map if it doesn't exist 630 * @param {Object} args 631 * @param {String|Number} args.callId 632 * @param {String|Object} args.plugin id of the video plugin or the plugin object itself 633 * @param {Object} args.window the parent window for this plugin (for popup support) 634 * @private 635 */ 636 add: function(args) { 637 var windowid; 638 if(!args) { 639 return; 640 } 641 if(!args.callId) { 642 return; 643 } 644 if(!args.window) { 645 args.window = window; 646 } 647 if(!this.calls[args.callId]) { 648 this.calls[args.callId] = []; 649 } 650 windowid = this.getWindowId({window:args.window}); 651 if(!this.calls[args.callId][windowid]) { 652 this.calls[args.callId][windowid] = {}; 653 } 654 if(typeof args.plugin === "string") { 655 this.calls[args.callId][windowid][args.plugin] = { 656 windowhandle: null, 657 plugin: null, 658 used: false 659 }; 660 var elem = args.window.document.getElementById(args.plugin); 661 if(elem && elem.windowhandle) { 662 this.update({window: args.window, callId: args.callId, plugin: elem}); 663 } 664 } else if(args.plugin && args.plugin.id) { 665 this.calls[args.callId][windowid][args.plugin.id] = { 666 windowhandle: args.plugin.windowhandle, 667 plugin: args.plugin, 668 used:false 669 }; 670 if(args.plugin.windowhandle && args.plugin.windowhandle !== "00000000") { 671 _plugin.api.addWindowToCall({callId: args.callId, windowhandle: args.plugin.windowhandle}); 672 this.calls[args.callId][windowid][args.plugin.id].used = true; 673 } 674 } 675 }, 676 /** 677 * removes a plugin object from a call 678 * @param {Object} args 679 * @param {String|Number} args.callId 680 * @param {String|Object} args.plugin 681 * @param {Object} args.window the parent window for this plugin (for popup support) 682 * @private 683 */ 684 remove: function(args) { 685 var windowid; 686 if(!args) { 687 return; 688 } 689 if(!args.callId) { 690 return; 691 } 692 if(!this.calls[args.callId]) { 693 return; 694 } 695 if(!args.window) { 696 args.window = window; 697 } 698 windowid = this.getWindowId({window:args.window, readOnly: true}); 699 if(windowid === -1 || !this.calls[args.callId][windowid]) { 700 return; 701 } 702 if(typeof args.plugin === "string" && this.calls[args.callId][windowid][args.plugin]) { 703 if(this.calls[args.callId][windowid][args.plugin].windowhandle) { 704 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][windowid][args.plugin].windowhandle}); 705 } 706 delete this.calls[args.callId][windowid][args.plugin]; 707 } 708 if(args.plugin && args.plugin.id) { 709 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: args.plugin.windowhandle}); 710 delete this.calls[args.callId][windowid][args.plugin.id]; 711 } 712 }, 713 /** 714 * updates all remote video plugins associated with a callid or the specific one passed in 715 * @param {Object} args 716 * @param {String|Number} args.callId 717 * @param {Object} [args.plugin] 718 * @param {Object} args.window the parent window for this plugin (for popup support) 719 * @private 720 */ 721 update: function(args) { 722 if(!args) { 723 return; 724 } 725 var win; 726 // Add any queued windows to the call if they've been loaded (i.e. have a windowhandle now) since being added 727 if(!args.plugin || !args.plugin.id) { 728 if(args.callId) { 729 if(this.calls[args.callId]) { 730 for(win in this.calls[args.callId]) { 731 if(this.calls[args.callId].hasOwnProperty(win)) { 732 for(var pluginId in this.calls[args.callId][win]) { 733 if(this.calls[args.callId][win].hasOwnProperty(pluginId)) { 734 if(!this.calls[args.callId][win][pluginId].plugin) { 735 this.calls[args.callId][win][pluginId].plugin = this.windowobjects[win].document.getElementById(pluginId); 736 } 737 if(this.calls[args.callId][win][pluginId].plugin && this.calls[args.callId][win][pluginId].plugin.windowhandle) { 738 if(this.calls[args.callId][win][pluginId].used) { 739 if(this.calls[args.callId][win][pluginId].windowhandle !== this.calls[args.callId][win][pluginId].plugin.windowhandle) { 740 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle}); 741 this.calls[args.callId][win][pluginId].windowhandle = this.calls[args.callId][win][pluginId].plugin.windowhandle; 742 _plugin.api.addWindowToCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle}); 743 } 744 } else { 745 this.calls[args.callId][win][pluginId].windowhandle = this.calls[args.callId][win][pluginId].plugin.windowhandle; 746 _plugin.api.addWindowToCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle}); 747 this.calls[args.callId][win][pluginId].used = true; 748 } 749 } 750 } 751 } 752 } 753 } 754 } 755 } 756 return; 757 } 758 // Check if the window handle has changed since last update 759 // If so, remove the old one, add the new one. 760 if(!args.window) { 761 args.window = window; 762 } 763 win = this.getWindowId({window:args.window, readOnly:true}); 764 if(win === -1) { 765 return; 766 } 767 for(var call in this.calls) { 768 if(this.calls.hasOwnProperty(call)) { 769 if(this.calls[call][win] && this.calls[call][win][args.plugin.id]) { 770 if(this.calls[call][win][args.plugin.id].windowhandle !== args.plugin.windowhandle) { 771 if(this.calls[call][win][args.plugin.id].windowhandle) { 772 _plugin.api.removeWindowFromCall({callId: call, windowhandle: this.calls[call][win][args.plugin.id].windowhandle}); 773 } 774 this.calls[call][win][args.plugin.id].windowhandle = args.plugin.windowhandle; 775 if(args.plugin.windowhandle) { 776 _plugin.api.addWindowToCall({callId: call, windowhandle: this.calls[call][win][args.plugin.id].windowhandle}); 777 } 778 } 779 } 780 } 781 } 782 }, 783 /** 784 * currently unused 785 * @param {Object} args 786 * @param {String|Number} args.callId 787 * @private 788 */ 789 addCall: function(args) { 790 if(args.callId) { 791 if(this.calls[args.callId]) { 792 return; 793 } else { 794 this.calls[args.callId] = []; 795 } 796 } 797 }, 798 /** 799 * delete a callId from the map, removing all window handles and clearing up 800 * @param {Object} args 801 * @param {String|Number} args.callId 802 * @private 803 */ 804 deleteCall: function(args) { 805 if(args.callId) { 806 if(this.calls[args.callId]) { 807 for(var win in this.calls[args.callId]) { 808 if(this.calls[args.callId].hasOwnProperty(win)) { 809 for(var plugin in this.calls[args.callId][win]) { 810 if(this.calls[args.callId][win].hasOwnProperty(plugin)) { 811 if(this.calls[args.callId][win][plugin].windowhandle) { 812 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][win][plugin].windowhandle}); 813 } 814 } 815 } 816 } 817 } 818 delete this.calls[args.callId]; 819 } 820 } 821 }, 822 dump: function() { 823 _log("Dumping Video Windows..."); 824 for(var call in this.calls) { 825 if(this.calls.hasOwnProperty(call)) { 826 _log(" callId: "+call); 827 for(var win in this.calls[call]) { 828 if(this.calls[call].hasOwnProperty(win)) { 829 _log(" window: "+this.windowobjects[win].document.location.pathname); 830 for(var pluginId in this.calls[call][win]) { 831 if(this.calls[call][win].hasOwnProperty(pluginId)) { 832 _log(" ID: " + pluginId + ", windowhandle: " + this.calls[call][win][pluginId].windowhandle + ", used: "+this.calls[call][win][pluginId].used); 833 } 834 } 835 } 836 } 837 } 838 } 839 } 840 841 }; 842 843 var videowindowloadedcallbacks = { 844 windowobjects: [], 845 getWindowId: function(args) { 846 var win = args.window; 847 var windowid = this.windowobjects.indexOf(win); 848 if(windowid === -1 && !args.readOnly) { 849 this.windowobjects.push(win); 850 windowid = this.windowobjects.indexOf(win); 851 this.callbacks[windowid] = {}; 852 } 853 return windowid; 854 }, 855 callbacks: [] 856 }; 857 858 /** 859 * called when a video plugin object is loaded or the window handle for a video plugin object changes 860 * @param pluginobject the DOM element (sort-of - not so much on IE) of the plugin object 861 * @param windowhandle the window handle of the plugin object - passed here because dereferencing it from the plugin object on IE sometimes causes a reference error (even checking typeof on it) 862 * @private 863 */ 864 var windowhandleupdated = function(pluginobject,windowhandle, win) { 865 if(!win) { 866 win = window; 867 } 868 _log("Updating plugin " + " on " + win.document.location.pathname + " windowhandle: " + windowhandle, pluginobject); 869 var element; 870 // updated window handle may be either a preview or a remote window - update both, updates will ignore if not already stored... 871 var windowid = videowindowloadedcallbacks.getWindowId({window:win}); 872 var updated = false; 873 for(var vidpluginid in videowindowloadedcallbacks.callbacks[windowid]) { 874 if(videowindowloadedcallbacks.callbacks[windowid].hasOwnProperty(vidpluginid)) { 875 try { 876 updated = true; 877 element = win.document.getElementById(vidpluginid); 878 previewwindows.update({"window": win, plugin: element, windowhandle: windowhandle}); 879 videowindowsbycall.update({"window":win, plugin: element, windowhandle: windowhandle}); 880 if(element && element.windowhandle && pluginobject.windowhandle && element.windowhandle === pluginobject.windowhandle) { 881 var onloaded = videowindowloadedcallbacks.callbacks[windowid][vidpluginid]; 882 if(!videowindowloadedcallbacks.callbacks[windowid][vidpluginid].wascalled) { 883 if(onloaded && $.isFunction(onloaded.callback)) { 884 try { 885 onloaded.callback(vidpluginid); 886 } catch(videoLoadedException) { 887 _log('Exception occurred in application videoLoaded callback',videoLoadedException); 888 if(typeof console !== "undefined" && console.trace) { 889 console.trace(); 890 } 891 } 892 } 893 videowindowloadedcallbacks.callbacks[windowid][vidpluginid].wascalled = true; 894 } 895 } 896 } 897 catch(e) { 898 _log("Warning: internal window handle update error", e); 899 if(typeof console !== "undefined" && console.trace) { 900 console.trace(); 901 } 902 // IE throws an exception, ff/chrome leaves windowhandle undefined on not fully instantiated plugins 903 } 904 } 905 } 906 //previewwindows.dump(); 907 //videowindowsbycall.dump(); 908 }; 909 var windowHandleUpdaters = { 910 windowobjects: [], 911 windowHandleUpdaters: [], 912 _getWindowId: function(args) { 913 var win = args.window; 914 var windowid = this.windowobjects.indexOf(win); 915 if(windowid === -1 && !args.readOnly) { 916 this.windowobjects.push(win); 917 windowid = this.windowobjects.indexOf(win); 918 } 919 return windowid; 920 }, 921 get: function(win) { 922 var windowId = this._getWindowId({window:win}); 923 if(!this.windowHandleUpdaters[windowId]) { 924 this.windowHandleUpdaters[windowId] = function(pluginobject,windowhandle) { 925 return(windowhandleupdated(pluginobject,windowhandle,win)); 926 }; 927 } 928 return this.windowHandleUpdaters[windowId]; 929 } 930 }; 931 /** 932 * global(window) level function object to handle video plugin object onLoad 933 * @type function 934 * @param {JSAPI} videopluginobject video plugin object (DOM Element) 935 * @returns undefined 936 */ 937 window._cwic_onVideoPluginLoaded = function(videopluginobject) { 938 try { 939 windowhandleupdated(videopluginobject, videopluginobject.windowhandle, window); 940 _log("_window updated"); 941 } 942 catch(e) { 943 // IE throws an exception when dereferencing a property on an invisible (display: none) object 944 } 945 _removeListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(window)); 946 _addListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(window)); 947 }; 948 949 950 /** 951 * global(window) level function to handle video plugins loaded in iframe/popup window 952 * Should be called from video object onLoad handler {@link $.fn.cwic-createVideoWindow}<br> 953 * Example onLoad function in iframe.html 954 * @example 955 * function onvideoPluginLoaded(obj) { 956 * window.parent._cwic_onPopupVideoPluginLoaded(obj, window); 957 * } 958 * @returns undefined 959 * @param {JSAPI} videopluginobject video plugin object (DOM Element) 960 * @param {DOMWindow} win iframe or popup window 961 * @public 962 */ 963 window._cwic_onPopupVideoPluginLoaded = function(videopluginobject, win) { 964 try { 965 windowhandleupdated(videopluginobject, videopluginobject.windowhandle, win); 966 _log("popup _window updated: " + win.document.location.pathname); 967 } 968 catch(e) { 969 // IE throws an exception when dereferencing a property on an invisible (display: none) object 970 } 971 _removeListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(win)); 972 _addListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(win)); 973 }; 974 975 976 /** 977 * Wait for the document to be ready, and try to load the plugin object.<br> 978 * If cwic was successfully initialized, call the options.ready handler, <br> 979 * passing some stored properties (possibly empty), <br> 980 * otherwise call the options.error handler<br> 981 * @param {Object} options Is a set of key/value pairs to configure the phone registration. See {@link $.fn.cwic-settings} for options. 982 * @example 983 * jQuery('#phone').cwic('init', { 984 * ready: function(defaults) { 985 * console.log('phone is ready'); 986 * }, 987 * error: function(status) { 988 * console.log('phone cannot be initialized : ' + status); 989 * }, 990 * log: function(msg, exception) { 991 * console.log(msg); if (exception) { console.log(exception); } 992 * }, 993 * errorMap: { 994 * // localized message for error code #17 995 * 17 : { message: 'Nom d'utilisateur ou mot de passe incorrect' } 996 * }, 997 * node: 'http://mynode.com:8080', 998 * predictDevice: function(args) { 999 * return settings.devicePrefix+args.username; 1000 * } 1001 * encodeBase64: function(str) { 1002 * encoded = myBase64Implementation(str); 1003 * return encoded; 1004 * } 1005 *}); 1006 */ 1007 function init(options) { 1008 _log(true, 'init', arguments); 1009 1010 var $this = this; 1011 1012 // the application can replace/extend the default error map 1013 if (typeof options.errorMap !== "undefined") { 1014 // extend the default errorMap 1015 $.each(options.errorMap, function(key, info) { 1016 1017 if (typeof info === "string") { 1018 errorMap[key] = $.extend({}, errorMap[key], { message: info }); 1019 } 1020 else if (typeof info === "object") { 1021 errorMap[key] = $.extend({}, errorMap[key], info); 1022 } 1023 else { 1024 _log('ignoring invalid custom error [key=' + key +']', info); 1025 } 1026 }); 1027 } 1028 1029 // extend the default settings with options 1030 $.extend(settings, options); 1031 1032 // this function is called by the browser after the cwic plugin object is loaded 1033 // see the <param> of the inserted plugin <object> 1034 /** 1035 * Phone Object onLoad handler 1036 * @returns undefined 1037 */ 1038 window._cwic_onPluginLoaded = function() { 1039 var error; 1040 try { 1041 var cwcObject = document.getElementById('cwc-plugin'); // re-select object 1042 1043 if (cwcObject) { 1044 // plugin is available, update global reference 1045 _plugin = {}; 1046 1047 _plugin.api = cwcObject; 1048 _plugin.version = _plugin.api.version; 1049 } 1050 1051 if (_plugin !== null) { 1052 _log("initialized plugin version " + _plugin.version.plugin + " (ecc " + _plugin.version.ecc + ")"); 1053 1054 var defaults = {},s,m,phoneRegistered=false; 1055 1056 s = $(location).get(0).search; 1057 if ((m = (/user=(.*?)($|&)/i).exec(s))) { defaults.user = m[1]; } 1058 if ((m = (/cucm=(.*?)($|&)/i).exec(s))) { defaults.cucm = m[1]; } 1059 1060 // automatically shutdown when the window unloads 1061 $(window).unload(function() { shutdown(); }); 1062 1063 var currState = _plugin.api.connectionStatus; 1064 if(currState === 'eReady') { 1065 phoneRegistered=true; 1066 } 1067 _registerSystemCallbacks($this); 1068 _registerCallChangeCallbacks($this); 1069 1070 _log('setting media port splitting to ' + settings.mediaPortSplitting); 1071 _plugin.api.MediaPortSplitting = settings.mediaPortSplitting; 1072 1073 var phoneMode = _plugin.api.mode; 1074 if ($.isFunction(settings.ready)) { 1075 try { 1076 settings.ready(defaults,phoneRegistered,phoneMode); 1077 } catch(readyException) { 1078 _log('Exception occurred in application ready callback',readyException); 1079 if(typeof console !== "undefined" && console.trace) { 1080 console.trace(); 1081 } 1082 } 1083 1084 } 1085 if(phoneRegistered) { 1086 var calls = _plugin.api.getCalls(); 1087 for(var i=0;i<calls.calls.length;i++) { 1088 _triggerConversationEvent($this, calls.calls[i], 'state'); 1089 } 1090 } 1091 1092 return; 1093 } 1094 else { 1095 error = errorMap.PluginNotAvailable; 1096 } 1097 } 1098 catch (e) { 1099 if(typeof console !== "undefined" && console.trace) { 1100 console.trace(); 1101 } 1102 _plugin = null; 1103 error = $.extend({}, errorMap.PluginNotAvailable, e); 1104 } 1105 1106 // TODO: Remove hardcoded string 1107 _triggerError($this, settings.error, 'cannot initialize Cisco Web Communicator', error); 1108 1109 }; // _cwic_onPluginLoaded 1110 1111 $(document.body).ready(function() { 1112 1113 if (_plugin === null) { 1114 // create plugin object and attach to DOM body 1115 1116 try { 1117 // try to load the ActiveX/NPAPI plugin, throw an error if it fails 1118 1119 if (!$.browser.jasmine) { 1120 if ($.browser.msie && parseInt($.browser.version, 10) >= 7) { 1121 // Internet Explorer 7+ 1122 try { 1123 var pluginExists = new ActiveXObject("CiscoSystems.CWCVideoCall"); 1124 // no exception, plugin is available 1125 // how to check plugin is enabled in IE ? 1126 pluginExists = null; 1127 } 1128 catch(dummy_e) { 1129 // exception, plugin is not available 1130 try { 1131 // check if previous release is installed 1132 var previousPluginExists = new ActiveXObject("ActivexPlugin.WebPhonePlugin.1"); 1133 // no exception. previous plugin is available 1134 previousPluginExists = null; 1135 throw getError('ReleaseMismatch'); 1136 } 1137 catch(ppe) { 1138 // no previous plugin, ignore silently 1139 } 1140 1141 throw getError('PluginNotAvailable'); 1142 } 1143 } 1144 1145 else if ($.browser.mozilla || $.browser.webkit) { 1146 // Mozilla/Webkit 1147 // check plugin availability through MIME types 1148 var pluginMimeType = navigator.mimeTypes["application/x-ciscowebcommunicator"]; 1149 if (typeof pluginMimeType === "undefined") { 1150 // plugin not available, check if any previous release is installed 1151 pluginMimeType = navigator.mimeTypes["application/x-ciscowebphone"]; 1152 if (typeof pluginMimeType !== "undefined") { 1153 // previous plugin is available 1154 throw getError('ReleaseMismatch'); 1155 } 1156 1157 throw getError('PluginNotAvailable'); 1158 } 1159 1160 if (pluginMimeType.enabledPlugin === null) { 1161 // additional flag indicating the plugin is installed but disabled 1162 var err = getError('PluginNotAvailable'); 1163 err.pluginDisabled = true; 1164 throw err; 1165 } 1166 } 1167 1168 else { 1169 throw getError('BrowserNotSupported'); 1170 } 1171 1172 $(document.body).append('<object id="cwc-plugin" width="1" height="1" type="application/x-ciscowebcommunicator"><param name="onload" value="_cwic_onPluginLoaded"></param></object>'); 1173 } 1174 } 1175 catch (e) { 1176 _plugin = null; 1177 _triggerError($this, settings.error, e); 1178 } 1179 } 1180 1181 }); // document ready 1182 1183 return $this; 1184 } // end of init 1185 1186 var videopluginid = 1; 1187 /** 1188 * Update the global registration object with information from the native plugin 1189 */ 1190 function _updateRegistration(state) { 1191 //registration.mode = _plugin.api.mode; 1192 1193 // get the available devices returned by the plugin 1194 var ret = _plugin.api.getAvailableDevices(); 1195 var devices = ret.devices; 1196 1197 // merge device information returned by the plugin 1198 $.each($.makeArray(devices), function(i, device) { 1199 if (device.name){ 1200 var deviceName = $.trim(device.name); 1201 registration.devices[deviceName] = $.extend({}, registration.devices[deviceName], device); 1202 // associate an array of lines to each device 1203 //registration.devices[deviceName].lines = _plugin.api.getAvailableLines(deviceName); 1204 } 1205 }); 1206 1207 var pluginDevice = '';//_plugin.api.PreferredDevice; 1208 if (pluginDevice !== '') { 1209 $.extend(registration.device, { name: pluginDevice }); 1210 } 1211 1212 // add device and line info except during logout 1213 if (state != 'eIdle') { 1214 registration.device = $.extend({}, _plugin.api.device); 1215 1216 registration.line = $.extend(registration.line, _plugin.api.line); 1217 } 1218 } 1219 1220 /** 1221 * Creates an object that can be passed to startConversation, addPreviewWindow or updateConversation('addRemoteVideoWindow'). 1222 * The object is inserted into the element defined by the jQuery context - e.g. jQuery('#placeholder').cwic('createVideoWindow') inserts the videowindow under jQuery('#placeholder') 1223 * @example $('#videocontainer').cwic('createVideoWindow',{id: 'videoplugin',success: function(pluginid) {$('#conversation').cwic('updateConversation',{'addRemoteVideoWindow': pluginid});}}); 1224 * @param {Object} [settings] Settings to use when creating the video render object 1225 * @param {String} [settings.id = generated] The DOM id of the element to be created 1226 * @param {Function} [settings.success] Called when the object is loaded and ready for use plugin id is passed as a parameter 1227 * @param {Function} [onload] Optional, not recommended for video windows created in the same window as the main phone plugin. 1228 * <br>Mandatory in popup windows or iframes, must call parent or opener {@link window._cwic_onPopupVideoPluginLoaded} in the onLoad handler. 1229 * <br>Single parameter is the videoplugin object that must be passed to the parent handler. 1230 * <br> 1231 * <br>NOTE: If video isn't supported on the platform, this function will just 'do nothing' and the success() callback will never be called. 1232 */ 1233 function createVideoWindow(settings) { 1234 var $this = this; 1235 var videoSupportedByPlugin = ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo); 1236 if(videoSupportedByPlugin) { 1237 settings = settings || {}; 1238 settings.window = settings.window || window; 1239 /* TODO: Remotely inject function into other window if possible 1240 if(settings.window !== window && !settings.window._cwic_onVideoPluginLoaded) { 1241 settings.window._cwic_onVideoPluginLoaded = _cwic_onVideoPluginLoaded; 1242 } */ 1243 var mimetype = "application/x-cisco-cwc-videocall"; 1244 var onload = settings.onload || "_cwic_onVideoPluginLoaded"; 1245 var callback = settings.success;// || videopluginloaded; 1246 var id = settings.id || '_cwic_vw'+videopluginid; 1247 videopluginid++; 1248 var windowid = videowindowloadedcallbacks.getWindowId({window:settings.window}); 1249 videowindowloadedcallbacks.callbacks[windowid][id] = {callback: callback, wascalled: false}; 1250 var elemtext='<object type="'+mimetype+'" id="'+id+'"><param name="onload" value="'+onload+'"></param></object>'; 1251 //jQuery(settings.window.document.getElementById($this.selector)).append(elemtext); 1252 jQuery($this).append(elemtext); 1253 } 1254 return $this; 1255 } 1256 /** 1257 * Assign a video window object to preview (self view) 1258 * @param {Object} args arguments object 1259 * @param {DOMWindow} [args.window] DOM Window that contains the plugin Object defaults to current window 1260 * @param {String|PluginObject} args.previewWindow id or DOM element of preview window 1261 */ 1262 function addPreviewWindow(args) { 1263 var $this = this; 1264 args.window = args.window || window; 1265 if(args.previewWindow) { 1266 previewwindows.add({plugin: args.previewWindow, "window": args.window}); 1267 //previewwindows.dump(); 1268 } 1269 return $this; 1270 } 1271 /** 1272 * Remove a video window object from preview (self view) 1273 * @example 1274 * $('#phone').cwic('removePreviewWindow',{previewWindow: 'previewVideoObjectID'}); 1275 * $('#phone').cwic('removePreviewWindow',{previewWindow: previewVideoObject, window: iFramePinPWindow}); 1276 * @param {Object} args arguments object 1277 * @param {DOMWindow} [args.window] DOM Window that contains the plugin Object defaults to current window 1278 * @param {String|Object} args.previewWindow id or DOM element of preview window 1279 */ 1280 function removePreviewWindow(args) { 1281 var $this = this; 1282 args.window = args.window || window; 1283 if(args.previewWindow) { 1284 previewwindows.remove({plugin: args.previewWindow, "window": args.window}); 1285 //previewwindows.dump(); 1286 } 1287 return $this; 1288 } 1289 /** 1290 * Shuts down the API<br> 1291 * <ul><li>Unregisters the phone</li> 1292 * <li>Unbinds all cwic events handlers</li> 1293 * <li>Clears all cwic data</li> 1294 * <li>Releases the CWC browser plugin instance</li></ul> 1295 * @example 1296 * jQuery(window).unload(function() { <br> 1297 * jQuery('#phone').cwic('shutdown'); <br> 1298 * }); <br> 1299 */ 1300 function shutdown() { 1301 _log(true, 'shutdown', arguments); 1302 1303 unregisterPhone(); 1304 1305 // unbind all cwic events handlers 1306 $(document).unbind('.cwic'); 1307 1308 // clear callbacks and logout if needed 1309 _reset(); 1310 1311 try { 1312 // release the plugin instance 1313 if (_plugin && _plugin.api && typeof _plugin.api.releaseInstance !== "undefined") { 1314 _plugin.api.releaseInstance(); 1315 } 1316 } 1317 catch(e) { 1318 } 1319 1320 //$('#cwc-plugin').remove(); 1321 _plugin = null; 1322 1323 } 1324 1325 /** 1326 * @private 1327 */ 1328 function _triggerAuthenticationResult($this, result) { 1329 _log(true, 'authenticationResult '+result); 1330 if(result === "eNoError") { 1331 if(registering.authenticatedCallback) { 1332 registering.authenticatedCallback(); 1333 } 1334 } else { 1335 if(registering.authenticatedCallback) { 1336 registering.authenticatedCallback = null; 1337 delete registering.authenticatedCallback; 1338 } 1339 _triggerError($this,registering.errorCb,getError(result,'LoginError'),result, {registration: registration}); 1340 } 1341 } 1342 /** 1343 * @private 1344 */ 1345 function _triggerProviderEvent($this,state) { 1346 _log(true, 'providerState ' + state); 1347 1348 var event = $.Event('system.cwic'); 1349 event.phone = { status: state }; 1350 1351 // update global registration and add it to the system event 1352 _updateRegistration(state); 1353 event.phone.registration = registration; 1354 1355 //providerStates.push(state); 1356 1357 if (state === 'eReady') { 1358 // check providerState contains 'AwaitingIpAddress' while RecoveryPending, 1359 // otherwise not really ready 1360 /* 1361 if ($.inArray('RecoveryPending', providerStates) != -1 && 1362 $.inArray('AwaitingIpAddress', providerStates) == -1) { 1363 return; 1364 } 1365 */ 1366 1367 // clear provider state history 1368 //providerStates = []; 1369 1370 // call success callback only if registering phone 1371 if (registering.registeringPhone || registering.switchingMode) { 1372 registering.registeringPhone = false; 1373 registering.switchingMode = false; 1374 1375 // finish registering 1376 1377 if (registering.successCb) { 1378 try { 1379 registering.successCb($.extend({}, registration, { 1380 cucm: $.makeArray(registering.CUCM), 1381 password: registering.password, 1382 mode: _plugin.api.mode, 1383 successfulCucm: { 1384 tftp: _plugin.api.successfulTftpAddress, 1385 cti: _plugin.api.successfulCtiAddress 1386 } 1387 })); 1388 } catch(successException) { 1389 _log('Exception occurred in application success callback',successException); 1390 if(typeof console !== "undefined" && console.trace) { 1391 console.trace(); 1392 } 1393 } 1394 } 1395 else { 1396 _log('warning: no registerPhone success callback'); 1397 } 1398 } 1399 1400 event.phone.ready = true; 1401 $this.trigger(event); 1402 var calls = _plugin.api.getCalls(); 1403 for(var i=0;i<calls.calls.length;i++) { 1404 _triggerConversationEvent($this, calls.calls[i], 'state'); 1405 } 1406 } 1407 else if (state === 'eIdle') { 1408 if(registering.unregisterCb) { 1409 registering.unregisterCb(); 1410 } 1411 $this.trigger(event); 1412 } 1413 else { 1414 $this.trigger(event); 1415 } 1416 } // end of _triggerProviderEvent 1417 1418 function _registerSystemCallbacks($this) { 1419 _addListener(_plugin.api,"connectionstatuschange",function(_state) { 1420 _triggerProviderEvent($this,_state); 1421 }); 1422 _addListener(_plugin.api,"connectionfailure",function(_status) { 1423 var errorKey = getError(_status, 'LoginError'); 1424 _log(true, _status,errorKey); 1425 _triggerError($this, registering.errorCb, errorKey, _status, { registration: registration }); 1426 }); 1427 _addListener(_plugin.api,"authenticationresult",function(result) { 1428 _triggerAuthenticationResult($this,result); 1429 }); 1430 _addListener(_plugin.api,"authenticationstatuschange",function(result) { 1431 _log(true, 'authenticationStatus '+result); 1432 //_triggerAuthenticationResult($this,result); 1433 }); 1434 _addListener(_plugin.api,"callcontrolmodechange",function(result) { 1435 if(result.phoneMode !== 'NoMode') { 1436 registration.mode = result.phoneMode; 1437 _triggerProviderEvent($this, 'ePhoneModeChanged'); 1438 } 1439 _log(true, 'callcontrolmodechange. Mode: ' + result.phoneMode + ', deviceName: ' + result.deviceName + ', DN: ' + result.lineDN ); 1440 }); 1441 1442 1443 } 1444 function _registerCallChangeCallbacks($this) { 1445 // subscribe to call updates 1446 1447 _addListener(_plugin.api,"callstatechange",function(call) { 1448 _triggerConversationEvent($this,call,'state'); 1449 }); 1450 1451 _addListener(_plugin.api, "videoresolutionchange", function(params) { 1452 _log(true, 'video resolution change detected for call ' + params.callId + 1453 '. Height: ' + params.height + 1454 ', width: ' + params.width, params); 1455 1456 // trigger a conversation event with a 'videoResolution' property 1457 _triggerConversationEvent($this, { 1458 callId: params.callId, 1459 videoResolution: { 1460 width: params.width, 1461 height: params.height 1462 } 1463 }, 1464 'render'); 1465 }); 1466 } 1467 1468 /** 1469 * Switch mode on a session that's already authorised <br> 1470 * @example 1471 * $('#phone').cwic('switchMode',{ 1472 * success: function(registration) { console.log("Phone is in "+registration.mode+" mode"); }, 1473 * error: function(err) { console.log("Error: "+error.message+" while switching mode"); }, 1474 * mode: "DeskPhone", 1475 * device: "SEP01234567" 1476 * }); 1477 * @param options 1478 * @param {Function} [options.progress] A handler called when the mode switch has passed pre-conditions.<br>If specified, the handler is called when the switchMode operation starts. 1479 * @param {Function} [options.success] A handler called when mode switch complete with registration as a parameter 1480 * @param {Function} [options.error] A handler called when the mode switch fails on pre-conditions. 1481 * @param {Function} [options.mode] The new mode 'SoftPhone'/'DeskPhone'. defaults to SoftPhone 1482 * @param {Function} [options.device] Name of the device (e.g. SEP012345678, CSFUSER) to control. defaults to picking first available 1483 * @param {Function} [options.line] Phone number of a line valid for the specified device (e.g. '0000'). defaults to picking first available 1484 * @param {Boolean} [options.forceRegistration] Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. If you use false and your CUCM device is already in use by another client, the error callback will be invoked with an eDeviceAlreadyRegistered error. Note that this feature requires CUCM 9.0 and currently is only supported on Windows. See GracefulRegistration doc for more info. 1485 */ 1486 function switchPhoneMode(options) { 1487 var ret; 1488 var $this = this; 1489 registering.successCb = $.isFunction(options.success) ? options.success : null; 1490 registering.errorCb = $.isFunction(options.error) ? options.error : null; 1491 registering.switchingMode = true; 1492 1493 //TODO: {phoneMode: registration.mode, deviceName: _predictDevice({username: registration.user}),lineDN: ""} 1494 var mode = options.mode || 'SoftPhone'; 1495 var device = options.device || (options.mode === 'SoftPhone'? _predictDevice({username: registration.user}) : ''); 1496 var linedn = options.line || ''; 1497 var forceRegistration = options.forceRegistration || false; 1498 _log('About to call switchMode with mode: ' + mode + ', deviceName: ' + device + ', lineDn: ' + linedn); 1499 ret = _plugin.api.switchMode({phoneMode: mode, deviceName: device, lineDN: linedn, forceRegistration: forceRegistration}); 1500 1501 if (ret.error) { 1502 if(options.error && $.isFunction(options.error)) { 1503 _triggerError($this, options.error, getError(ret), {message: ret}); 1504 } 1505 } else { 1506 if (options.progress && $.isFunction(options.progress)) { 1507 try { 1508 options.progress({message: ret}); 1509 } catch(progressException) { 1510 _log('Exception occurred in application switchPhoneMode progress callback',progressException); 1511 if(typeof console !== "undefined" && console.trace) { 1512 console.trace(); 1513 } 1514 } 1515 1516 } 1517 } 1518 1519 return this; 1520 } 1521 1522 /** 1523 * Register phone to CUCM (SIP register) 1524 * @param args A map with: 1525 * @param {String} args.user The CUCM end user name (required) 1526 * @param {String|Object} args.password String - clear password. Object - {encrypted: encoded password, cipher:"cucm"} 1527 * @param {boolean} [args.authenticate] A flag to specify if user should be authenticated against CUCM (optional). 1528 * If the user is already authenticated then the application has the option to bypass this additional authentication against Cisco Unified Communications Manager. Authentication can be made against the CCMCIP interface of CUCM (HTTP Basic). 1529 * This additional authentication requires a server-side component to be deployed (see the node parameters of the init function). 1530 * @param {String|Object|Array} args.cucm The list of CUCM(s) to attempt to register with (required).<br> 1531 * If String, it will be used as both the CCMCIP and TFTP address.<br> 1532 * If Array, a list of String or Object as described above. 1533 * @param {String[]} args.cucm.tftp TFTP addresses 1534 * @param {String[]} [args.cucm.ccmcip] CCMCIP address (will use tftp values if not present). 1535 * @param {String} [args.mode] Register the phone in this mode. Available modes are "SoftPhone" or "DeskPhone".Default of intelligent guess is applied after a device is selected.<br> 1536 * @param {Function} [args.devicesAvailable(devices)] Callback called after successful authentication. 1537 * If this callback is not specified, cwic applies the default device selection algorithm. An array of {@link device} objects is passed so the application can select the device.<br> 1538 * Return one of:<ul><li>device object to register with that device</li><li>null to fall back to default selection algorithm</li><li>false to stop registration (raises an error).</li></ul> 1539 * @param {Function} [args.error(err)] Callback called if the registration fails. 1540 * @param {Boolean} args.useCcmcip Authenticate using ccmcip (overrides settings if present). 1541 * @param {Boolean} args.forceRegistration Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. If you use false and your CUCM device is already in use by another client, the error callback will be invoked with an eDeviceAlreadyRegistered error. Please note that this feature requires CUCM 9.0 and currently is only supported on Windows. See GracefulRegistration doc for more info. 1542 * @param {Function} [args.success(registration)] Callback called when registration succeeds. A {@link registration} object is passed to the callback: 1543 * registerPhone examples <br> 1544 * @example 1545 * // ************************************* 1546 * // register with lab CUCM in default mode (SoftPhone) 1547 * jQuery('#phone').cwic('registerPhone', { 1548 * user: 'fbar', 1549 * password: 'secret', // clear password 1550 * cucm: '1.2.3.4', 1551 * success: function(registration) { 1552 * console.log('registered in mode ' + registration.mode); 1553 * console.log('registered with device ' + registration.device.name); 1554 * } 1555 * }); 1556 * @example 1557 * // ************************************* 1558 * // register with Alpha CUCM in DeskPhone mode 1559 * jQuery('#phone').cwic('registerPhone', { 1560 * user: 'fbar', 1561 * password: { 1562 * encoded: 'GJH$&*"@$%$^BLKJ==', 1563 * cipher: 'cucm' 1564 * }, 1565 * mode: 'DeskPhone', 1566 * cucm: '1.2.3.4', 1567 * success: function(registration) { 1568 * console.log('registered in mode ' + registration.mode); 1569 * console.log('registered with device ' + registration.device.name); 1570 * } 1571 * ); 1572 * @example 1573 * // ************************************* 1574 * // register with Alpha CUCM in SoftPhone mode, select ECP{user} device 1575 * jQuery('#phone').cwic('registerPhone', { 1576 * user: 'tvanier', 1577 * password: { 1578 * encoded: 'GJH$&*"@$%$^BLKJ==', 1579 * cipher: 'cucm' 1580 * }, 1581 * mode: 'SoftPhone', 1582 * cucm: { 1583 * ccmcip: ['1.2.3.4'], 1584 * tftp: ['1.2.3.5'] 1585 * }, 1586 * devicesAvailable: function(devices) { 1587 * for (var i = 0; i < devices.length; i++) { 1588 * var device = devices[i]; 1589 * if (device.name.match(/^ECP/i)) { 1590 * return device; 1591 * } // starts with 'ECP' 1592 * } 1593 * return false; // stop registration if no ECP{user} device found 1594 * }, 1595 * success: function(registration) { 1596 * console.log('registered in mode ' + registration.mode); 1597 * console.log('registered with device ' + registration.device.name); 1598 * }, 1599 * error: function(status) { 1600 * console.log('cannot register phone: ' + status); 1601 * } 1602 * ); 1603 */ 1604 function registerPhone(args) { 1605 _log(true, 'registerPhone', arguments); 1606 1607 var $this = this; 1608 1609 if(!about().capabilities['multireg'] && typeof args.forceRegistration !== "undefined") { 1610 _log('Warning: Attempting to use forceRegistration parameter on unsupported platform. Parameter will be ignored.'); 1611 } 1612 1613 // flag to indicate cwic is in the process of registering a phone 1614 registering.registeringPhone = true; 1615 1616 // reset global registration object 1617 registration = { 1618 user: args.user, 1619 mode: args.mode || "SoftPhone", 1620 devices: {}, 1621 forceRegistration: args.forceRegistration || false, 1622 authenticate: typeof args.authenticate === 'boolean' ? args.authenticate : false 1623 }; 1624 1625 // validate password and make it an object 1626 var password = args.password; 1627 var clearPassword = ''; 1628 1629 var devicesAvailableCb = $.isFunction(args.devicesAvailable) ? args.devicesAvailable : null; 1630 1631 registering.successCb = $.isFunction(args.success) ? args.success : null; 1632 registering.errorCb = $.isFunction(args.error) ? args.error : null; 1633 registering.CUCM = args.cucm; 1634 registration.useCcmcip = false; 1635 if(typeof args.useCcmcip !== "undefined") { 1636 registration.useCcmcip = args.useCcmcip; 1637 } else { 1638 registration.useCcmcip = args.useCcmcip; 1639 } 1640 1641 if (!_plugin) { 1642 //TODO: remove hardcoded string 1643 return _triggerError($this, registering.errorCb, errorMap.PluginNotAvailable, 'Plugin is not available or has not been initialized', { registration: registration }); 1644 } 1645 1646 if (typeof password === "string") { 1647 // clear password, encrypt it 1648 password = { cipher: 'cucm', encrypted: _plugin.api.encryptCucmPassword(args.password) }; 1649 if (registration.authenticate) { clearPassword = args.password; } 1650 } else if (typeof password !== "object" || (password.cipher !== "cucm" && password.cipher !== "base64")) { 1651 return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'invalid password (type ' + typeof password + ')', { registration: registration }); 1652 } 1653 1654 // make preferredDevice a string (possibly empty) 1655 var preferredDevice = args.device || _plugin.api.PreferredDevice; 1656 if (typeof preferredDevice === "object") { 1657 preferredDevice = (preferredDevice.name ? preferredDevice.name : ''); 1658 } 1659 1660 // make preferredLine a string (possibly empty) 1661 var preferredLine = args.line || _plugin.api.PreferredLine; 1662 if (typeof preferredLine === "object") { 1663 preferredLine = (preferredLine.directoryNumber ? preferredLine.directoryNumber : ''); 1664 } 1665 1666 // build an array of strings (CUCM addresses) 1667 var cucm = []; 1668 var ccmcip = []; 1669 1670 $.each($.makeArray(args.cucm), function(i, elem) { 1671 if (typeof elem === "string") { 1672 // cucm string can be 'lab call manager 1.2.3.4' 1673 var a = elem.split(' '); 1674 cucm.push(a[a.length-1]); 1675 } 1676 else if (typeof elem === "object") { 1677 if ($.isArray(elem.ccmcip)) { ccmcip = elem.ccmcip; } 1678 if ($.isArray(elem.tftp)) { cucm = elem.tftp; } 1679 1680 if (!$.isArray(elem.ccmcip) && !$.isArray(elem.tftp)) { 1681 _log('registerPhone: no ccmcip and tftp properties for cucm'); 1682 } 1683 } 1684 else { 1685 _log('registerPhone: ignoring cucm argument of type ' + typeof elem); 1686 } 1687 }); 1688 1689 _log('registerPhone: ' + cucm.length + ' cucm addresses'); 1690 _log(true, cucm); 1691 1692 _log("registerPhone of user=" + registration.user + 1693 ' (authenticate=' + registration.authenticate + ') in mode="' + registration.mode + '"'); 1694 1695 // reset first 1696 //_reset(); 1697 1698 if (!registration.user || registration.user === '') { 1699 return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'Missing user name', { registration: registration }); 1700 } 1701 1702 if (!$.isArray(cucm) || cucm.length < 1) { 1703 return _triggerError($this, registering.errorCb, errorMap.NoCallManagerConfigured, 'Missing CUCM address', { registration: registration }); 1704 } 1705 1706 if (!registration.mode.match(/^(SoftPhone|DeskPhone)$/)) { 1707 return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'Invalid phone mode "' + registration.mode + '"', { registration: registration }); 1708 } 1709 1710 //_plugin.api.PreferredPhoneMode = registration.mode; 1711 1712 1713 _plugin.api.TftpAddressList = cucm; 1714 _plugin.api.CtiAddressList = cucm; 1715 _plugin.api.CcmcipAddressList = (ccmcip.length > 0)? ccmcip : cucm; 1716 1717 // keep track of previous states 1718 //var providerStates = []; 1719 1720 1721 // is the plugin already ready ? 1722 var currState = _plugin.api.connectionStatus; 1723 if (currState === 'eReady') { 1724 _triggerProviderEvent($this,currState); 1725 } 1726 registering.password = password; 1727 if (!registration.authenticate || registration.mode === "DeskPhone") { 1728 1729 if (registration.mode === "DeskPhone" && (!preferredDevice || !preferredDevice.match(/^\s*SEP/i))) { 1730 preferredDevice = ''; 1731 preferredLine = ''; 1732 } 1733 1734 //_plugin.api.PreferredDevice = preferredDevice; 1735 //_plugin.api.PreferredLine = preferredLine; 1736 1737 if(currState !== 'eReady') { 1738 // CUCM user password is encrypted 1739 var ret = "Did nothing"; 1740 if(password.encrypted) { 1741 registering.authenticatedCallback = function() { 1742 /* 1743 * name 1744 * description 1745 * model 1746 * modelDescription 1747 * isSoftPhone 1748 * isDeskPhone 1749 * lineDNs[] 1750 * serviceState 1751 */ 1752 var ret = _plugin.api.getAvailableDevices(); 1753 var _devices = ret.devices; 1754 _updateRegistration(currState); 1755 if(devicesAvailableCb) { 1756 try { 1757 devicesAvailableCb(_devices,registration.mode,function(phoneMode,deviceName,lineDN) { 1758 var res = _plugin.api.connect({phoneMode: phoneMode, deviceName: deviceName, lineDN: lineDN, forceRegistration: registration.forceRegistration}); 1759 if(res.error) { 1760 var error = getError(res.error);//errorMap[res.error] ? errorMap[res.error] : errorMap.LoginError; 1761 return _triggerError($this, registering.errorCb, error, res.error, { registration: registration }); 1762 } 1763 }); 1764 } catch(devicesAvailableException) { 1765 _log('Exception occurred in application devicesAvailable callback',devicesAvailableException); 1766 if(typeof console !== "undefined" && console.trace) { 1767 console.trace(); 1768 } 1769 } 1770 } else { 1771 var deviceName = ""; 1772 // If user has specified a device, use it to connect.... 1773 if(preferredDevice) { 1774 deviceName = preferredDevice; 1775 } else { //....otherwise, use the first available one 1776 for(var i=0;i<_devices.length;i++) { 1777 if(registration.mode==="SoftPhone" && _devices[i].isSoftPhone) { 1778 deviceName = _devices[i].name; 1779 break; 1780 } 1781 if(registration.mode==="DeskPhone" && _devices[i].isDeskPhone) { 1782 deviceName = _devices[i].name; 1783 break; 1784 } 1785 } 1786 } 1787 var res = _plugin.api.connect({phoneMode: registration.mode, deviceName: deviceName, lineDN: '', forceRegistration: registration.forceRegistration}); 1788 if(res.error) { 1789 var error = getError(res.error);//errorMap[res.error] ? errorMap[res.error] : errorMap.LoginError; 1790 return _triggerError($this, registering.errorCb, error, res.error, { registration: registration }); 1791 } 1792 } 1793 1794 registering.authenticatedCallback = null; 1795 delete registering.authenticatedCallback; 1796 }; 1797 ret = _plugin.api.authenticate({username:registration.user, password:password.encrypted, useCcmcip: settings.useCcmcip}); 1798 } else { 1799 ret = _plugin.api.connect({phoneMode: registration.mode, deviceName: _predictDevice({username: registration.user}),lineDN: "", forceRegistration: registration.forceRegistration}); 1800 } 1801 if(ret.error) { 1802 var error = getError(ret.error);//errorMap[ret['Error']] ? errorMap[ret['Error']] : errorMap.LoginError; 1803 return _triggerError($this, registering.errorCb, error, ret.error, { registration: registration }); 1804 } 1805 } 1806 } 1807 else { 1808 // need to authenticate against CUCM here until the plugin supports it 1809 1810 // to be called for each CUCM address until success 1811 var cucmAddresses = [].concat(cucm); 1812 1813 var authenticateCcmcip = function() { 1814 1815 var cucmAddress = cucmAddresses.shift(); 1816 1817 if (typeof cucmAddress === "undefined") { 1818 _triggerError($this, registering.errorCb, errorMap.NoCucmFound, 'cannot register phone', { registration: registration }); 1819 return; 1820 } 1821 1822 _log(true, 'authenticate against CUCM "' + cucmAddress + '"'); 1823 1824 $.ajax({ 1825 url: settings.node + '/phoneconfig/devices' + '?ccmcip=' + cucmAddress, 1826 beforeSend : function(req) { 1827 var auth = 'Basic ' + _encodeBase64(registration.user + ':' + clearPassword); 1828 req.setRequestHeader('Authorization', auth); 1829 }, 1830 error: function(jqXHR, textStatus, errorThrown) { 1831 _log(true, 'CCMCIP failure with CUCM "' + cucmAddress + '" : ' + textStatus, errorThrown); 1832 authenticateCcmcip(); // try next CUCM address 1833 }, 1834 success: function(devices) { 1835 registration.cucm = { 1836 ccmcip: [cucmAddress], 1837 tftp: [cucmAddress] 1838 }; 1839 1840 var selectedDevice = null; 1841 var defaultDevice = null; 1842 1843 // default device selection algorithm 1844 $.each(devices, function(i, device) { 1845 1846 _log(true, 'device=' + device.name + ' model="' + device.model + '"'); 1847 1848 if (typeof device.name === "undefined" || typeof device.model === "undefined") { 1849 // ignore this device and continue 1850 return; 1851 } 1852 1853 if (device.model.match(/^\s*Cisco\s+Unified\s+Client\s+Services\s+Framework\s*$/i)) { 1854 device.csf = true; 1855 1856 // in SoftPhone mode, select a CSF device only 1857 if (registration.mode === "SoftPhone") { 1858 if (device.name === preferredDevice) { 1859 selectedDevice = device; 1860 } 1861 1862 // select a device whose name starts with ECP in priority, first CSF device otherwise 1863 if (device.name.match(/^ECP/i) || !defaultDevice) { 1864 defaultDevice = device; 1865 } 1866 } 1867 } 1868 1869 if (device.name.match(/^\s*SEP/i)) { 1870 device.deskphone = true; 1871 1872 if (registration.mode === "DeskPhone") { 1873 if (device.name === preferredDevice) { 1874 selectedDevice = device; 1875 } 1876 1877 // in DeskPhone mode, select first device whose name starts with SEP 1878 if (!defaultDevice) { 1879 defaultDevice = device; 1880 } 1881 } 1882 } 1883 1884 registration.devices[$.trim(device.name)] = 1885 $.extend({}, registration.devices[$.trim(device.name)], device); 1886 }); 1887 1888 _log(true, 'default device is ' + (defaultDevice === null ? defaultDevice : defaultDevice.name)); 1889 1890 if (devicesAvailableCb) { 1891 // application wants to select device by itself 1892 try { 1893 selectedDevice = devicesAvailableCb(devices, registration.mode); 1894 } catch(devicesAvailableException) { 1895 _log('Exception occurred in application devicesAvailable callback',devicesAvailableException); 1896 if(typeof console !== "undefined" && console.trace) { 1897 console.trace(); 1898 } 1899 } 1900 1901 1902 // false means stop registering (null means use default selection) 1903 if (selectedDevice === false) { 1904 _log('registration interrupted'); 1905 return; 1906 } 1907 } 1908 1909 if (selectedDevice === null) { 1910 selectedDevice = defaultDevice; 1911 } 1912 1913 if (!selectedDevice) { 1914 // TODO: remove hardcoded string 1915 return _triggerError($this, registering.errorCb, errorMap.NoDevicesFound, 'no device found', { registration: registration }); 1916 } 1917 1918 if ((registration.mode !== "SoftPhone" && selectedDevice.csf) || 1919 (registration.mode !== "DeskPhone" && selectedDevice.deskphone)) { 1920 // TODO: remove hardcoded string, convert error string to map of mode, deviceName 1921 return _triggerError($this, registering.errorCb, errorMap.NoDevicesFound, 1922 'cannot register in ' + registration.mode + ' mode with device "' + selectedDevice.name + '"', { registration: registration }); 1923 } 1924 1925 _log('selected device is "' + selectedDevice.name +'"'); 1926 1927 _plugin.api.PreferredDevice = selectedDevice.name; 1928 1929 registration.device = selectedDevice; 1930 1931 _plugin.api.connect({phoneMode: registration.mode, deviceName: _plugin.api.PreferredDevice, lineDN: '', forceRegistration: registration.forceRegistration}); 1932 } // success 1933 }); // $.ajax 1934 1935 }; // function authenticate 1936 1937 authenticateCcmcip(); 1938 } 1939 1940 return $this; 1941 } // end of registerPhone 1942 1943 /** <br> 1944 * Unregisters a phone from CUCM:<ul> 1945 * <li>Ends any active call if this is the last instance or forceLogout is set to true.</li> 1946 * <li>Unbinds all cwic event handlers</li> 1947 * <li>In softphone mode, SIP unregisters, in deskphone mode, closes the CTI connection.</li> 1948 * <li>Calls the optional complete handler (always called)</li></ul> 1949 * @param args Is a set of key/value pairs to configure the phone unregistration. 1950 * @param {function} [args.complete] Callback called when unregistration is complete.<br> 1951 * If specified, the handler is always called, even if the phone was not registered first, or if the unregistrations caused errors. 1952 * @param {boolean} args.forceLogout: If true, end the phone session even if registered in other instances. 1953 * unregisterPhone examples 1954 * @example 1955 * // ************************************* 1956 * // unregister phone 1957 * jQuery('#phone') 1958 * .unbind('.cwic') // optional, done by unregisterPhone 1959 * .cwic('unregisterPhone', { 1960 * complete: function() { 1961 * console.log('phone is unregistered'); 1962 * } 1963 * }); 1964 */ 1965 function unregisterPhone() { 1966 _log(true, 'unregisterPhone', arguments); 1967 1968 var $this = this; 1969 1970 1971 if(typeof arguments[0] === "object" && typeof arguments[0].forceLogout !== 'undefined' && arguments[0].forceLogout) { 1972 _plugin.api.logout(); 1973 1974 // reset global registration object 1975 registration = {devices:{}}; 1976 } 1977 1978 _reset(); 1979 1980 if (typeof arguments[0] === "object" && typeof arguments[0].complete !== "undefined") { 1981 // call complete callback 1982 var complete = arguments[0].complete; 1983 registering.unregisterCb = function() { 1984 try { 1985 complete(); 1986 } catch(completeException) { 1987 _log('Exception occurred in application unregister complete callback',completeException); 1988 if(typeof console !== "undefined" && console.trace) { 1989 console.trace(); 1990 } 1991 } 1992 1993 registering.unregisterCb = null; 1994 }; 1995 } 1996 1997 return $this; 1998 } 1999 2000 function _reset() { 2001 // clear all cwic data 2002 $('.cwic-data').removeData('cwic'); 2003 2004 // unbind/unregister event callbacks ? 2005 } 2006 2007 function _triggerConversationEvent($this, conversation, topic) { 2008 var conversationId = conversation.callId; 2009 var conversationState = conversation.callState; 2010 2011 2012 // determine participant name and number 2013 var participant = {}; 2014 // TODO: handle "Forwarded" and possibly "None" 2015 if (conversation.callType === "Outgoing") { 2016 participant.name = conversation.calledPartyName; 2017 participant.recipient = 2018 conversation.calledPartyDirectoryNumber !== "" ? conversation.calledPartyDirectoryNumber : conversation.calledPartyNumber; 2019 } 2020 2021 if (conversation.callType === "Incoming") { 2022 participant.name = conversation.callingPartyName; 2023 participant.recipient = 2024 conversation.callingPartyDirectoryNumber !== "" ? conversation.callingPartyDirectoryNumber : conversation.callingPartyNumber; 2025 } 2026 2027 // select the conversation container with class cwic-conversation-{conversationId} 2028 var container = $('.cwic-conversation-' + conversationId); 2029 2030 // if no container, select the outgoing conversation (see startConversation) 2031 if (container.length === 0) { 2032 container = $('.cwic-conversation-outgoing'); 2033 2034 // in deskphone mode, container may not exist yet if conversation was initiated from deskphone 2035 //if (container.length == 0 && conversation.callType == "Outgoing") { 2036 //container = $('<div>').addClass('cwic-conversation-outgoing'); 2037 //} 2038 } 2039 2040 // at this point container may be empty, which means the conversation is incoming 2041 2042 var data = container.data('cwic') || {}; 2043 2044 _log(true, 'conversation id=' + conversationId + ' state=' + conversation.callState || data.state,conversation); 2045 2046 // extend conversation 2047 conversation = $.extend({}, data, conversation, { 2048 id: conversationId, 2049 state: conversationState || data.state, 2050 media: 'audio', 2051 participant: $.extend(data.participant, participant) 2052 }); 2053 2054 /* ECC call states and old skittles/webphone states 2055 OnHook : Disconnected 2056 OffHook : Created 2057 Ringout : RemotePartyAlerting 2058 Ringin : Alerting 2059 Proceed : Ringin on Deskphone while on a call amongst others 2060 Connected : Connected 2061 Hold : Held 2062 RemHold : "Passive Held" 2063 Resume : ? 2064 Busy : n/a (connected) 2065 Reorder : Failed 2066 Conference : n/a 2067 Dialing : Dialing 2068 RemInUse : "Passive not held" 2069 HoldRevert : n/a 2070 Whisper : n/a 2071 Parked : n/a 2072 ParkRevert : n/a 2073 ParkRetrieved : n/a 2074 Preservation : n/a 2075 WaitingForDigits : na/ ? Overlapdial capability ? 2076 Spoof_Ringout : n/a 2077 */ 2078 // check for an incoming call - based on the following arcane conditions: 2079 // Empty container and one of the following: 2080 // * Incoming/Created = ringing 2081 // * Incoming/Proceed = ringing (cti mode, already on a call) 2082 // * Ringin = ringing 2083 if ((((conversation.state === "Proceed" || conversation.state === "Created") && conversation.callType==="Incoming")|| conversation.state === "Ringin") && container.length === 0) { 2084 // new container for incoming call, application is supposed to attach it to the DOM 2085 container = $('<div>').addClass('cwic-data cwic-conversation cwic-conversation-' + conversationId).data('cwic', conversation); 2086 $this.trigger('conversationIncoming.cwic', [conversation, container]); 2087 return; 2088 } 2089 2090 // If we can originate a call, onHook does not mean the call has ended - it means it's just about to start 2091 else if ((conversation.state === "OnHook" && !conversation.capabilities.canOriginateCall) || !conversation.exists) { 2092 videowindowsbycall.deleteCall({callId: conversationId}); 2093 if (container.length === 0) { 2094 _log('warning: no container for ended conversation ' + conversationId); 2095 $this.trigger('conversationEnd.cwic', [conversation]); 2096 return; 2097 } 2098 2099 container 2100 .removeData('cwic') 2101 .removeClass('cwic-data cwic-conversation cwic-conversation-' + conversation.id) 2102 .trigger('conversationEnd.cwic', [conversation]); 2103 return; 2104 } 2105 2106 else { 2107 if (conversation.state === "OffHook" || conversation.state === "Connected") { 2108 2109 // store media connection time 2110 if (typeof conversation.connect === "undefined" && conversation.state === "Connected") { 2111 if (container.length === 0) { 2112 container = $('<div>').addClass('cwic-conversation cwic-conversation-' + conversationId); 2113 } 2114 $.extend(conversation, { connect: new Date() }); 2115 container.data('cwic', conversation); 2116 } 2117 2118 // store start time and trigger start event only once 2119 if (typeof conversation.start === "undefined") { 2120 if (container.length === 0) { 2121 container = $('<div>'); 2122 } 2123 $.extend(conversation, { start: new Date() }); 2124 container.data('cwic', conversation); 2125 2126 container 2127 .removeClass('cwic-conversation-outgoing') 2128 .addClass('cwic-conversation cwic-conversation-' + conversationId) 2129 .data('cwic', conversation); 2130 2131 $this.trigger('conversationStart.cwic', [conversation, container]); 2132 return; 2133 } 2134 } 2135 2136 if (container.length === 0) { 2137 // if we've just switched to deskphone mode and there's already a call, create a container div 2138 // or if we've just opened a new tab, we also need to trigger a conversation start for an ongoing call 2139 container = $('<div>').data('cwic',conversation).addClass('cwic-conversation cwic-conversation-' + conversationId); 2140 if(conversation.exists) { 2141 $this.trigger('conversationStart.cwic', [conversation, container]); 2142 return; 2143 } else { 2144 $this.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event 2145 return; 2146 } 2147 _log('warning: no container for updated conversation ' + conversationId); 2148 } else { 2149 container.data('cwic',conversation); 2150 } 2151 2152 videowindowsbycall.update({callId: conversationId}); 2153 container.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event 2154 } 2155 2156 } // function _triggerConversationEvent 2157 2158 /** 2159 * _triggerError(target, [callback], [code], [data]) <br> 2160 * <br> 2161 * - target (Object): a jQuery selection where to trigger the event error from <br> 2162 * - callback (Function): an optional callback to be called call with the error. if specifed, prevents the generic error event to be triggered <br> 2163 * - code (Number): an optional cwic error code (defaults to 0 – Unknown) <br> 2164 * - data (String, Object): some optional error data, if String, used as error message. if Object, used to extend the error. <br> 2165 * <br> 2166 * cwic builds an error object with the following properties: <br> 2167 * code: a pre-defined error code <br> 2168 * message: the error message (optional) <br> 2169 * any other data passed to _triggerError or set to errorMap (see the init function) <br> 2170 * <br> 2171 * When an error event is triggered, the event object is extended with the error properties. <br> 2172 * <br> 2173 */ 2174 function _triggerError() { 2175 var $this = arguments[0]; // target (first mandatory argument) 2176 var errorCb = null; 2177 2178 // the default error 2179 var error = $.extend({ details: [] }, errorMap.Unknown); 2180 2181 // extend error from arguments 2182 for (var i=1; i<arguments.length; i++) { 2183 var arg = arguments[i]; 2184 2185 // is the argument a specific error callback ? 2186 if ($.isFunction(arg)) { errorCb = arg; } 2187 2188 else if (typeof arg === "string") { error.details.push(arg); } 2189 2190 else if (typeof arg === "object") { $.extend(error, arg); } 2191 2192 } // for 2193 2194 _log(error.message, error); 2195 2196 // if specific error callback, call it 2197 if (errorCb) { 2198 try { 2199 errorCb(error); 2200 } catch(errorException) { 2201 _log('Exception occurred in application error callback',errorException); 2202 if(typeof console !== "undefined" && console.trace) { 2203 console.trace(); 2204 } 2205 } 2206 2207 } 2208 else { 2209 // if no specific error callback, raise generic error event 2210 var event = $.Event('error.cwic'); 2211 $.extend(event, error); 2212 $this.trigger(event); 2213 } 2214 2215 return $this; 2216 } 2217 2218 /** 2219 * @param {Object} [conversation] Can be a new object to start a new conversation or an existing conversation which you wish to answer 2220 * @param {String} [conversation.id] Unique identifier of the conversation. 2221 * @param {Object|Array} conversation.participant Conversation participants, does not include the local user. May be asingle participant Object or an array of participant Objects. 2222 * @param {String} conversation.participant.recipient The phone number or jabber id of the participant. 2223 * @param {String} [conversation.participant.name] The participant name. 2224 * @param {String} [conversation.participant.photosrc] A suitable value for the src attribute of an <img> element. 2225 * @param {String} [conversation.media] Media used to start the conversation.Contains the media used to join participants: audio, video or chat.<br> 2226 * If not specified, depends on all participant's recipients: if phone number, default media is "audio", if jabber id, default media is "chat". 2227 * @param {String} [conversation.state] Current state of the conversation. Can be OffHook, Ringing, Connected, OnHook, Reorder. 2228 * @param {Date} [conversation.start] Start time. Defined on resolution update only. 2229 * @param {Date} [conversation.connect] Media connection time. Defined on resolution update only. 2230 * @param {Object} [conversation.videoResolution] Resolution of the video conversation, contains width and height properties. Defined on resolution update only. 2231 * @param {String|Object} [conversation.container] The HTML element which contains the conversation (optional). Conversation events are triggered on this element. 2232 * If String, specifies a jQuery selector If Object, specifies a jQuery wrapper of matched elements(s). 2233 * By default container is $(this), that is the first element of the matched set startConversation is called on. 2234 * @param {String} [conversation.subject] The subject of the conversation to start. 2235 * @param {Function} [conversation.error(status)] A function to be called if the conversation cannot be started (optional). An error status (String) is passed. 2236 * @param {String} [conversation.videoDirection] The video media direction: 'Inactive' or undefined (audio only by default), 'SendOnly', 'RecvOnly' or 'SendRecv'. 2237 * @param {Object} [conversation.remoteVideoWindow] The video object (must be of mime type application/x-cisco-cwc-videocall). 2238 * @param {DOMWindow} [conversation.window] DOM window that contains the remoteVideoWindow (default to this DOM window) required if specifying a video object on another window (popup/iframe). 2239 * @description Start a conversation with a participant. 2240 * <br>If conversation contain both an id and a state property, cwic assumes you want to answer that incoming conversation, in this case starting the passed conversation means accepting(answering) it. 2241 * @example 2242 * // start an audio conversation with element #foo as container 2243 * jQuery('#phone').cwic('startConversation', { 2244 * participant: { 2245 * recipient: '1234' 2246 * }, 2247 * container: '#foo' 2248 * }); 2249 * // start an audio conversation with a contact (call work phone number) 2250 * jQuery('#conversation').cwic('startConversation', { 2251 * participant: { 2252 * recipient: '1234', 2253 * displayName: 'Foo Bar', 2254 * screenName: ' fbar', 2255 * phoneNumbers: { 2256 * work: '1234', 2257 * mobile: '5678' 2258 * } 2259 * } 2260 * }); 2261 * // start a room chat (ad-hoc) 2262 * jQuery('#conversation').cwic('startConversation', { 2263 * participant: [ 2264 * 'thomas@domain.com', 'niall@domain.com', 'david@domain.com', 2265 * 'martin@domain.com', 'jing@domain.com' 2266 * ], 2267 * media: 'chat', 2268 * subject: 'The Cisco Jabber SDK looks great !', 2269 * error: function(status) { 2270 * console.log('chat cannot be started: ' + status); 2271 * } 2272 * }); 2273 * // answer an incoming conversation (input has an id property) 2274 * // see another example about the conversationIncoming event 2275 * jQuery('#conversation').cwic('startConversation', { 2276 * participant: { 2277 * recipient: '1234' 2278 * }, 2279 * id: '612', 2280 * state: 'Ringin' 2281 * }); 2282 * // answer an incoming conversation with video 2283 * jQuery('#conversation').cwic('startConversation', 2284 * jQuery.extend(conversation,{ 2285 * videoDirection: (sendingVideo ? 'SendRecv':''), 2286 * remoteVideoWindow: 'remoteVideoWindow', // pass id 2287 * id: callId 2288 * })); 2289 * // answer an incoming conversation with video object hosted in popoutwindow 2290 * jQuery('#conversation').cwic('startConversation', 2291 * jQuery.extend(conversation,{ 2292 * videoDirection: (sendingVideo ? 'SendRecv':''), 2293 * remoteVideoWindow: $('#remoteVideoWindow', popoutwindow.document)[0] // pass object setting jQuery context to popoutwindow's document 2294 * window: popoutwindow, 2295 * id: callId 2296 * }); 2297 * // answer an incoming conversation without video 2298 * jQuery('#callcontainer').cwic('startConversation', conversation); 2299 */ 2300 function startConversation() { 2301 _log(true, 'startConversation', arguments); 2302 2303 var $this = this; 2304 2305 var callsettings = arguments[0] || $this.data('cwic') || {}; 2306 var windowhandle,videoDirection; 2307 2308 if ($this.length === 0) { 2309 // TODO: remove hardcoded string 2310 return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation with empty selection'); 2311 } 2312 2313 // container is the jQuery wrapper of the video container 2314 var container = $this; 2315 if (typeof callsettings.container === "string") { container = $(callsettings.container); } 2316 else if (typeof callsettings.container === "object") { container = callsettings.container; } 2317 container = container.first(); 2318 2319 if (typeof callsettings.id !== "undefined") { 2320 // start an incoming conversation 2321 container.addClass('cwic-data cwic-conversation cwic-conversation-' + callsettings.id).data('cwic', callsettings); 2322 2323 if (arguments.length >= 1 ) { 2324 videoDirection = callsettings.videoDirection; 2325 if(callsettings.remoteVideoWindow) { 2326 videowindowsbycall.add({callId: callsettings.id, plugin: callsettings.remoteVideoWindow}); 2327 if(callsettings.remoteVideoWindow.windowhandle) { 2328 windowhandle = callsettings.remoteVideoWindow.windowhandle; 2329 } 2330 } 2331 } else { 2332 videoDirection = ""; 2333 } 2334 2335 var answerObject = { 2336 callId: callsettings.id, 2337 videoDirection: videoDirection 2338 }; 2339 if(windowhandle) { 2340 answerObject.windowhandle = windowhandle; 2341 } 2342 _plugin.api.answer(answerObject); 2343 } 2344 else { 2345 // start an outgoing conversation 2346 var participant = callsettings.participant || {}; 2347 2348 if (typeof participant === "string") { 2349 participant = { recipient: participant }; 2350 } 2351 2352 if (typeof participant.recipient === "undefined") { 2353 // TODO: remove hardcoded string 2354 return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation: undefined or empty recipient'); 2355 } 2356 2357 container.addClass('cwic-data cwic-conversation cwic-conversation-outgoing').data('cwic', { participant: participant }); 2358 //var call = _plugin.api.getCall({ callId: -1 }); 2359 2360 //if(_hasCapability(call,'OverlapDial')) { 2361 //_plugin.api.overlapDial(participant.recipient); 2362 //} else { 2363 if (container.is(':hidden')) { 2364 _log(true, 'startConversation - warning: container is hidden'); 2365 } 2366 2367 // for now use jQuery dimensions and offset utilities 2368 // TO DO: offset() does not work with hidden elements, and does not support margins/borders, see http://api.jquery.com/offset/ 2369 //var containerOffset = container.offset(); 2370 //var top = settings.top || containerOffset.top; 2371 //var left = settings.left || containerOffset.left; 2372 //var width = settings.width || container.width(); 2373 //var height = settings.height || container.height(); 2374 2375 //_log(true, 'startConversation container top=' + top + ' left=' + left + ' width=' + width + ' height=' + height); 2376 2377 var originateObject = { 2378 recipient: participant.recipient, 2379 videoDirection: callsettings.videoDirection 2380 }; 2381 if(callsettings.remoteVideoWindow && callsettings.remoteVideoWindow.windowhandle) { 2382 originateObject.windowhandle = callsettings.remoteVideoWindow.windowhandle; 2383 } 2384 2385 var res = _plugin.api.originate(originateObject); 2386 2387 if (res.error) { 2388 _log(true, 'originate result', res); 2389 // TODO: remove hardcoded string 2390 _triggerError($this, callsettings.error, 'cannot start conversation', { nativeError: res.error }); 2391 } 2392 if(res.callId && res.callId >=1) { 2393 if(callsettings.remoteVideoWindow) { 2394 callsettings.window = callsettings.window || window; 2395 videowindowsbycall.add({callId: res.callId, plugin: callsettings.remoteVideoWindow, window: callsettings.window}); 2396 } 2397 } 2398 //} 2399 } 2400 2401 return $this; 2402 } 2403 2404 /** 2405 * @description Ends a conversation. <br> 2406 * @param{boolean} iDivert: If it is a true, the conversation is diverted to the default recipient *(voicemail for example, sometimes referred as "iDivert", configured by the admin). 2407 * Triggers a conversationEnd event. 2408 * @param{String|Object} id A conversation identifier (String) or an Object containing an id property.<br> 2409 * endConversation examples <br> 2410 * @example 2411 * // typeof input is string 2412 * jQuery('#phone').cwic('endConversation', '1234'); 2413 * // or 2414 * jQuery('#phone').cwic('updateConversation', conversation.id); 2415 * // typeof input is object 2416 * jQuery('#phone').cwic('endConversation', conversation); 2417 * // let cwic find the conversation data attached to #conversation 2418 * jQuery('#conversation').cwic('endConversation'); 2419 * // DIVERT 2420 * jQuery('#phone').cwic('endConversation', true, '1234'); 2421 * jQuery('#myconversation').cwic('endConversation', true); 2422 * 2423 */ 2424 function endConversation() { 2425 _log(true, 'endConversation', arguments); 2426 2427 var $this = this; 2428 2429 if ($this.length === 0) { return $this; } 2430 2431 var iDivert = null; 2432 var conversation = null; 2433 var conversationId = null; 2434 2435 if (arguments.length === 0) { 2436 conversation = $this.data('cwic'); 2437 if(!conversation) { 2438 // TODO: remove hardcoded string 2439 return _triggerError($this, 'cannot end conversation: no conversation exists for this element'); 2440 } 2441 conversationId = conversation.id; 2442 } 2443 else if (arguments.length === 1) { 2444 iDivert = typeof arguments[0] === "boolean" ? arguments[0] : null; 2445 conversation = typeof arguments[0] === "object" ? arguments[0] : $this.data('cwic'); 2446 conversationId = typeof arguments[0] === "string" ? arguments[0] : conversation.id; 2447 } 2448 else if (arguments.length === 2) { 2449 iDivert = typeof arguments[0] === "boolean" ? arguments[0] : null; 2450 conversation = typeof arguments[1] === "object" ? arguments[1] : $this.data('cwic'); 2451 conversationId = typeof arguments[1] === "string" ? arguments[1] : conversation.id; 2452 } 2453 2454 if (!conversationId) { 2455 // TODO: remove hardcoded string 2456 return _triggerError($this, errorMap.InvalidArguments, 'cannot end conversation: undefined or empty conversation id'); 2457 } 2458 2459 if (iDivert) { 2460 // need to check capabilities first 2461 conversation = conversation || $('.cwic-conversation-' + conversationId).data('cwic'); 2462 2463 if (!conversation) { 2464 // TODO: remove hardcoded string 2465 return _triggerError($this, 'cannot iDivert - undefined conversation'); 2466 } 2467 2468 if(!conversation.capabilities || !conversation.capabilities.canImmediateDivert) { 2469 // TODO: remove hardcoded string 2470 return _triggerError($this, errorMap.MissingCapability, 'cannot iDivert - missing capability', { conversation: conversation }); 2471 } 2472 2473 _log(true, 'iDivert conversation', conversation); 2474 2475 _plugin.api.iDivert({ callId: conversationId }); 2476 } 2477 else { 2478 _log(true, 'end conversation', conversation); 2479 _plugin.api.endCall({ callId: conversationId }); 2480 } 2481 2482 return $this; 2483 } 2484 /** 2485 * @description Updates an existing conversation.<br> 2486 * This function controls the call allowing the following operations<ul> 2487 * <li>hold call</li> 2488 * <li>resume call</li> 2489 * <li>mute call</li> 2490 * <li>unmute call</li> 2491 * <li>mute audio only</li> 2492 * <li>mute video only</li> 2493 * <li>unmute audio only</li> 2494 * <li>unmute video only</li> 2495 * <li>add video window for remote sender</li> 2496 * <li>remove video window for remote sender</li> 2497 * <li>update video preference on a call video escalate/de-escalate</li> 2498 * <li>conference two calls together</li> 2499 * <li>transfer a call</li> 2500 * </ul> 2501 * @param {String|Object} update Update a started conversation. update can be: <br> 2502 * A String: hold, resume, mute, unmute, muteAudio, muteVideo, unmuteAudio, unmuteVideo.<br> 2503 * An Object: contains one or more writable conversation properties to update e.g. videoDirection.<br> 2504 * Triggers a conversationUpdate event. 2505 * @param {String|Object} id A conversation identifier (String) or Object containing an id property <br> 2506 * @example 2507 * // typeof input is string HOLD/RESUME 2508 * jQuery('#phone').cwic('updateConversation', 'hold', '1234') 2509 * jQuery('body').cwic('updateConversation', 'hold', conversation.id); 2510 * jQuery('#myid').cwic('updateConversation', 'hold', conversation); 2511 * // typeof input is object 2512 * jQuery('#conversation').cwic('updateConversation', 'hold'); 2513 * // resume the same conversation, 2514 * // let cwic find the conversation data attached to #conversation 2515 * jQuery('#conversation').cwic('updateConversation', 'resume'); 2516 * // MUTE/UNMUTE 2517 * // typeof input is string 2518 * jQuery('#phone').cwic('updateConversation', 'mute', '1234'); 2519 * jQuery('body').cwic('updateConversation', 'mute', conversation.id); 2520 * jQuery('#myid').cwic('updateConversation', 'mute', conversation); 2521 * // typeof input is object <br> 2522 * jQuery('#conversation').cwic('updateConversation', 'mute'); 2523 * // unmute the same conversation, 2524 * // let cwic find the conversation data attached to #conversation 2525 * jQuery('#conversation').cwic('updateConversation', 'unmute'); 2526 * 2527 * // add/remove video object in this (default) DOMWindow 2528 * jQuery('#conversation').cwic('updateConversation', 2529 * { 'addRemoteVideoWindow':videoObject }); 2530 * jQuery('#conversation').cwic('updateConversation', 2531 * { 'removeRemoteVideoWindow':videoObject }); 2532 * // add/remove video object from another DOMWindow 2533 * jQuery('#conversation').cwic('updateConversation', 2534 * { 'addRemoteVideoWindow':videoObject, window:popupWindow }); 2535 * jQuery('#conversation').cwic('updateConversation', 2536 * { 'removeRemoteVideoWindow':videoObject, window:popupWindow }); 2537 * 2538 * // Escalate to video 2539 * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'SendRecv'}); // implied source call is call associated with conversation div 2540 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation.id}); // source call id passed 2541 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation}); // source call passed 2542 * // De-escalate from video 2543 * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'Inactive'}); // implied source call is call associated with conversation div 2544 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation.id}); // source call id passed 2545 * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation}); // source call passed 2546 * 2547 * // Transfer call to target callid 2548 * jQuery('#conversation').cwic('updateConversation', {'transferCall':callId}); // implied source call is call associated with conversation div 2549 * jQuery('#phone').cwic('updateConversation', {'transferCall':callId}, conversation.id}); // source call id passed 2550 * jQuery('#phone').cwic('updateConversation', {'transferCall':callId}, conversation}); // source call passed 2551 * 2552 * // Join target callId to source call 2553 * jQuery('#conversation').cwic('updateConversation', {'joinCall':callId}); // implied source call is call associated with conversation div 2554 * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation.id}); // source call id passed 2555 * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation}); // source call passed 2556 */ 2557 function updateConversation() { 2558 _log(true, 'updateConversation', arguments); 2559 2560 var $this = this; 2561 if ($this.length === 0) { return $this; } 2562 2563 // mandatory first argument 2564 var update = arguments[0]; 2565 2566 // find conversation information 2567 var conversation = null; 2568 var conversationId = null; 2569 if (typeof arguments[1] === "object") { 2570 conversation = arguments[1]; 2571 conversationId = conversation.id; 2572 } 2573 else if (typeof arguments[1] === "undefined") { 2574 conversation = $this.data('cwic'); // attached conversation object 2575 if (typeof conversation === "object") { conversationId = conversation.id; } 2576 } 2577 else { 2578 conversationId = arguments[1]; 2579 conversation = $('.cwic-conversation-' + conversationId).data('cwic') || $this.data('cwic'); 2580 } 2581 2582 if (!conversationId || !conversation) { 2583 // TODO: remove hardcoded string 2584 return _triggerError($this, errorMap.InvalidArguments, 'cannot update conversation: undefined or empty conversation id'); 2585 } 2586 2587 var nativeResult = {}; 2588 if (typeof update === "string") { 2589 if (update.match(/^hold$/i)) { 2590 nativeResult = _plugin.api.hold({ callId: conversationId }); 2591 } 2592 else if (update.match(/^resume$/i)) { 2593 nativeResult = _plugin.api.resume({ callId: conversationId }); 2594 } 2595 else if (update.match(/^mute$/i)) { 2596 nativeResult = _plugin.api.mute({ callId: conversationId }); 2597 } 2598 else if (update.match(/^unmute$/i)) { 2599 nativeResult = _plugin.api.unmute({ callId: conversationId }); 2600 } 2601 else if (update.match(/^muteAudio$/i)) { 2602 nativeResult = _plugin.api.mute({ callId: conversationId, muteAudio: true }); 2603 } 2604 else if (update.match(/^muteVideo$/i)) { 2605 nativeResult = _plugin.api.mute({ callId: conversationId, muteVideo: true }); 2606 } 2607 else if (update.match(/^unmuteAudio$/i)) { 2608 nativeResult = _plugin.api.unmute({ callId: conversationId, unmuteAudio: true }); 2609 } 2610 else if (update.match(/^unmuteVideo$/i)) { 2611 nativeResult = _plugin.api.unmute({ callId: conversationId, unmuteVideo: true }); 2612 } 2613 else { 2614 // TODO: remove hardcoded string 2615 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation) - '+update, arguments); 2616 } 2617 2618 if (nativeResult.error) { 2619 return _triggerError($this, getError(nativeResult.error), nativeResult.error); 2620 } 2621 } 2622 else if (typeof update === "object") { 2623 var foundWritable = false; 2624 2625 if (update.transferCall) { 2626 foundWritable = true; 2627 nativeResult = _plugin.api.transferCall({ callId: conversationId, transferCallId: update.transferCall}); 2628 if (nativeResult.error) { 2629 return _triggerError($this, getError(nativeResult.error,'NativePluginError'),'transferCall',nativeResult); 2630 } 2631 } 2632 if (update.joinCall) { 2633 foundWritable = true; 2634 nativeResult = _plugin.api.joinCalls({ joinCallId: conversationId, callId: update.joinCall}); 2635 if (nativeResult.error) { 2636 return _triggerError($this, getError(nativeResult.error,'NativePluginError'),'joinCall',nativeResult); 2637 } 2638 } 2639 if (update.videoDirection) { 2640 foundWritable = true; 2641 nativeResult = _plugin.api.setVideoDirection({ callId: conversationId, videoDirection: update.videoDirection }); 2642 if (nativeResult.error) { 2643 return _triggerError($this, errorMap.NativePluginError, 'videoDirection', nativeResult.error); 2644 } 2645 } 2646 if(update.addRemoteVideoWindow) { 2647 foundWritable = true; 2648 videowindowsbycall.add({callId: conversationId, plugin: update.addRemoteVideoWindow, "window": update.window}); 2649 //videowindowsbycall.dump(); 2650 } 2651 if(update.removeRemoteVideoWindow) { 2652 foundWritable = true; 2653 videowindowsbycall.remove({callId: conversationId, plugin: update.addRemoteVideoWindow, "window": update.window}); 2654 //videowindowsbycall.dump(); 2655 } 2656 if (!foundWritable) { 2657 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments); 2658 } 2659 } 2660 else { 2661 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments); 2662 } 2663 2664 return $this; 2665 } 2666 /** 2667 * Sends digit (String) as Dual-Tone Multi-Frequency (DTMF) 2668 * @example 2669 * // SEND DTMF EXAMPLE 2670 * jQuery('#phone').cwic('sendDTMF', '5', '1234'); 2671 * jQuery('#mydiv').cwic('sendDTMF', '3', conversation.id); 2672 * jQuery('body').cwic('sendDTMF', '7', conversation); 2673 * jQuery('#conversation').cwic('sendDTMF', '1'); 2674 * @param {String} digit Dual-Tone Multi-Frequency (DTMF) digit to send. Does not trigger any event. 2675 * @param {String|Object} [id] a {String} conversation identifier or an {Object} containing an id property 2676 */ 2677 function sendDTMF() { 2678 _log(true, 'sendDTMF'); // don't send dtmf digits to logger 2679 2680 var $this = this; 2681 var digit = null; 2682 var conversation = $this.data('cwic'); 2683 var conversationId = conversation ? conversation.id : null; 2684 2685 // inspect arguments 2686 if (arguments.length > 0) { 2687 digit = typeof arguments[0] === "string" ? arguments[0] : null; 2688 2689 if (arguments.length > 1) { 2690 if (typeof arguments[1] === "object") { 2691 conversation = arguments[1]; 2692 conversationId = conversation.id; 2693 } 2694 else if (typeof arguments[1] === "string") { 2695 conversationId = arguments[1]; 2696 } 2697 } 2698 } 2699 2700 if (typeof digit !== "string" || !conversationId) { 2701 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (sendDTMF)', arguments); 2702 } 2703 2704 var nativeResult = _plugin.api.sendDTMF({ 2705 callId: conversationId, 2706 digit: digit 2707 }); 2708 2709 if (nativeResult.error) { 2710 return _triggerError($this, errorMap.NativePluginError, nativeResult.error); 2711 } 2712 2713 return $this; 2714 } 2715 2716 /** 2717 * Versions, states and capabilities 2718 * @returns {aboutObject} 2719 */ 2720 function about() { 2721 _log(true, 'about', arguments); 2722 2723 /* 2724 Versioning scheme: Release.Major.Minor.Revision 2725 2726 Release should be for major feature releases (e.g. video) 2727 Major for an API-breaking ship within a release (or additional APIs that won't work without error checking on previous plugins). 2728 Minor for non API-breaking builds - e.g. bug fix releases that strongly recommend updating the plugin 2729 Revision for unique build tracking. 2730 */ 2731 2732 var ab = { 2733 javascript: { 2734 version: '2.1.0.60779' 2735 }, 2736 jquery: { 2737 version: $.fn.jquery 2738 }, 2739 plugin: (_plugin === null) ? null : { version: _plugin.version }, 2740 states: { 2741 system: (!_plugin || !_plugin.api) ? 'unknown' : _plugin.api.connectionStatus 2742 }, 2743 device: (!_plugin || !_plugin.api) ? {exists: false, inService: false, lines: {}, model: -1, modelDescription: '', name: ''} : _plugin.api.device, 2744 capabilities: { 2745 video: ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo), 2746 // Undocumented capability - used to differentiate between MR versions on Mac and Win that contain 2747 // differing abilities when it comes to multireg support. Because (for this release) Mac doesn't support video, 2748 // multireg support == video support 2749 multireg: ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo) 2750 }, 2751 upgrade: {} 2752 }; 2753 2754 var m = ab.javascript.version.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); 2755 if (m) { 2756 ab.javascript.release = m[1]; 2757 ab.javascript.major = m[2]; 2758 ab.javascript.minor = m[3]; 2759 ab.javascript.revision = m[4]; 2760 } 2761 2762 if (ab.plugin) { 2763 m = ab.plugin.version.plugin.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); 2764 if (m) { 2765 ab.plugin.release = m[1]; 2766 ab.plugin.major = m[2]; 2767 ab.plugin.minor = m[3]; 2768 ab.plugin.revision = m[4]; 2769 } 2770 2771 // compare javascript and plugin versions to advise about upgrade 2772 if (ab.javascript.release > ab.plugin.release) { 2773 // release mismatch, upgrade plugin 2774 ab.upgrade.plugin = 'mandatory'; 2775 } 2776 else if (ab.javascript.release < ab.plugin.release) { 2777 // release mismatch, upgrade javascript 2778 ab.upgrade.javascript = 'mandatory'; 2779 } 2780 else if (ab.javascript.release == ab.plugin.release) { 2781 // same release, compare major 2782 if (ab.javascript.major > ab.plugin.major) { ab.upgrade.plugin = 'mandatory'; } 2783 else if (ab.javascript.major < ab.plugin.major) { ab.upgrade.javascript = 'mandatory'; } 2784 else if (ab.javascript.major == ab.plugin.major) { 2785 // same release.major, compare minor 2786 if (ab.javascript.minor > ab.plugin.minor) { ab.upgrade.plugin ='recommended'; } 2787 else if (ab.javascript.minor < ab.plugin.minor) { ab.upgrade.javascript = 'recommended'; } 2788 } 2789 } 2790 } 2791 else { 2792 // no plugin available 2793 ab.upgrade.plugin = 'mandatory'; 2794 } 2795 2796 return ab; 2797 } 2798 2799 function getInstanceId() { 2800 _log(true, 'getInstanceId'); 2801 return _plugin.api.instanceId; 2802 } 2803 2804 // a map with all exposed methods 2805 var methods = { 2806 about: about, 2807 init : init, 2808 shutdown: shutdown, 2809 rebootIfBroken: rebootIfBroken, 2810 registerPhone: registerPhone, 2811 switchPhoneMode: switchPhoneMode, 2812 unregisterPhone: unregisterPhone, 2813 startConversation: startConversation, 2814 updateConversation: updateConversation, 2815 endConversation: endConversation, 2816 createVideoWindow: createVideoWindow, 2817 addPreviewWindow: addPreviewWindow, 2818 removePreviewWindow: removePreviewWindow, 2819 sendDTMF: sendDTMF, 2820 getInstanceId: getInstanceId 2821 }; 2822 2823 // the jQuery plugin 2824 /** 2825 * @description 2826 * CWIC is a jQuery plugin to access the Cisco Web Communicator<br> 2827 * Audio and Video media require the CWC browser plugin to be installed <br> 2828 * <h3>Fields overview</h3> 2829 * <h3>Methods overview</h3> 2830 * All cwic methods are called in the following manner<br> 2831 * <pre class="code">$('#selector').cwic('method',parameters)</pre><br> 2832 * <h3>Events overview</h3> 2833 * All events are part of the cwic namespace. For example: 2834 * <ul> 2835 * <li>conversationStart.cwic</li> 2836 * <li>system.cwic</li> 2837 * <li>error.cwic</li> 2838 * </ul> 2839 * <h4>Example conversation events:</h4> 2840 * These are conversation-related events that can be triggered by the SDK.<br> 2841 * The event handlers are passed the conversation properties as a single object. For example:<br> 2842 * @example 2843 * // start an audio conversation with phone a number and bind to conversation events 2844 * jQuery('#conversation') 2845 * .cwic('startConversation', '+1 234 567') // container defaults to $(this) 2846 * .bind('conversationStart.cwic', function(event, conversation, container) { 2847 * console.log('conversation has just started'); 2848 * // container is jQuery('#conversation') 2849 * }) 2850 * .bind('conversationUpdate.cwic', function(event, conversation) { 2851 * console.log('conversation has just been updated'); 2852 * }) 2853 * .bind('conversationEnd.cwic', function(event, conversation) { 2854 * console.log('conversation has just ended'); 2855 * }); 2856 * @example 2857 * // listen for incoming conversation 2858 * jQuery('#phone') 2859 * .bind('conversationIncoming.cwic', function(event, conversation, container) { 2860 * console.log('incoming conversation with id ' + conversation.id); 2861 * // attach the "toast" container to the DOM and bind to events 2862 * container 2863 * .appendTo('#phone') 2864 * .bind('conversationUpdate.cwic', function(event, conversation) { 2865 * // update on incoming conversation 2866 * }) 2867 * .bind('conversationEnd.cwic', function(event, conversation) { 2868 * // incoming conversation has ended 2869 * container.remove(); 2870 * }); 2871 * // suppose UI has a button with id 'answer' 2872 * jQuery('#answer').click(function() { 2873 * // answer the incoming conversation 2874 * // conversation has an id property, so startConversation accepts it 2875 * // use element #conversation as container 2876 * jQuery('#conversation').cwic('startConversation', conversation); 2877 * // remove incoming container 2878 * container.remove(); 2879 * }); 2880 * }); 2881 * @class 2882 * @static 2883 * @param {String} method The name of the method to call 2884 * @param {Variable} arguments trailing arguments are passed to the specific call see methods below 2885 */ 2886 $.fn.cwic = function( method ) { 2887 2888 try { 2889 // Method calling logic 2890 if ( methods[method] ) { 2891 return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 )); 2892 } 2893 else if ( typeof method === 'object' || ! method ) { 2894 return methods.init.apply( this, arguments ); 2895 } 2896 else { 2897 throw method + ': no such method on jQuery.cwic'; 2898 } 2899 } 2900 catch(e) { 2901 if(typeof console !== "undefined" && console.trace) { 2902 console.trace(); 2903 } 2904 _triggerError(this, e); 2905 } 2906 }; 2907 }(jQuery)); 2908