1 /* 2 * Copyright (C) 2012-2013 DFKI GmbH 3 * Deutsches Forschungszentrum fuer Kuenstliche Intelligenz 4 * German Research Center for Artificial Intelligence 5 * http://www.dfki.de 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a 8 * copy of this software and associated documentation files (the 9 * "Software"), to deal in the Software without restriction, including 10 * without limitation the rights to use, copy, modify, merge, publish, 11 * distribute, sublicense, and/or sell copies of the Software, and to 12 * permit persons to whom the Software is furnished to do so, subject to 13 * the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included 16 * in all copies or substantial portions of the Software. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 */ 26 27 28 29 define(['jquery', 'constants', 'commonUtils', 'configurationManager', 'dictionary', 'logger', 'module'], 30 /** 31 * The MediaManager gives access to audio in- and output functionality. 32 * 33 * Depending on its configuration, the MediaManager loads different implementation modules 34 * (<em>plugins</em>) that realize the interface-functions differently. 35 * 36 * See directory <code>mmirf/env/media</code> for available plugins. 37 * 38 * This "class" is a singleton - so that only one instance is in use.<br> 39 * 40 * @class 41 * @name MediaManager 42 * @memberOf mmir 43 * @static 44 * 45 * @requires jQuery.extend 46 * @requires jQuery.Deferred 47 * 48 * TODO remove / change dependency on forBrowser: constants.isBrowserEnv()!!! 49 */ 50 function( 51 jQuery, constants, commonUtils, configurationManager, Dictionary, Logger, module 52 ){ 53 //the next comment enables JSDoc2 to map all functions etc. to the correct class description 54 /** @scope mmir.MediaManager.prototype */ 55 56 /** 57 * The instance that holds the singleton MediaManager object. 58 * @private 59 * @type MediaManager 60 * @memberOf MediaManager# 61 */ 62 var instance = null; 63 64 /** 65 * default configuration for env-settings "browser" and "cordova": 66 * 67 * -> may be overwritten by settings in the configuration file. 68 * e.g. adding the following JSON data to config/configuration.json: 69 * <pre> 70 * "mediaManager": { 71 * "plugins": { 72 * "browser": ["html5AudioOutput.js", 73 * "html5AudioInput.js", 74 * "maryTextToSpeech.js", 75 * {"mod": "webkitAudioInput.js", "ctx": "chrome"} 76 * ], 77 * "cordova": ["cordovaAudioOutput.js", 78 * "nuanceAudioInput.js", 79 * "nuanceTextToSpeech.js", 80 * {"mod": "androidAudioInput.js", "ctx": "native"}, 81 * {"mod": "androidTextToSpeech.js", "ctx": "native"}, 82 * {"mod": "maryTextToSpeech.js", "ctx": "web"} 83 * ] 84 * } 85 * } 86 * </pre> 87 * 88 * @private 89 * @type PlainObject 90 * 91 * @memberOf MediaManager# 92 */ 93 var _defaultPlugins = { 94 'browser': ['waitReadyIndicator.js', 95 'html5AudioOutput.js', 96 'html5AudioInput.js', 97 'maryTextToSpeech.js' 98 ], 99 'cordova': ['waitReadyIndicator.js', 100 'cordovaAudioOutput.js', 101 'androidAudioInput.js', 102 'maryTextToSpeech.js' 103 ] 104 }; 105 106 /** 107 * Load an media-module implementation from plugin file. 108 * 109 * @param {String} filePath 110 * @param {Function} successCallback 111 * @param {Function} failureCallback 112 * @param {String} [execId] 113 * 114 * @private 115 * @function 116 * 117 * @memberOf MediaManager# 118 */ 119 var loadPlugin = function loadPlugin(filePath, successCallback, failureCallback, execId){ 120 try { 121 122 commonUtils.loadScript(constants.getMediaPluginPath() + filePath, function(){ 123 124 if (typeof newMediaPlugin !== 'undefined' && newMediaPlugin){ 125 126 newMediaPlugin.initialize(function(exportedFunctions){ 127 128 if(execId){ 129 130 //create new "execution context" if necessary 131 if(typeof instance.ctx[execId] === 'undefined'){ 132 133 instance.ctx[execId] = {}; 134 135 } 136 137 //import functions and properties into execution-context: 138 var func; 139 for(var p in exportedFunctions){ 140 141 if(exportedFunctions.hasOwnProperty(p)){ 142 143 //only allow extension of the execution-context, no overwriting: 144 if(typeof instance.ctx[execId][p] === 'undefined'){ 145 146 func = exportedFunctions[p]; 147 if(typeof func === 'function'){ 148 149 //need to "re-map" the execution context for the functions, 150 // so that "they think" they are actually executed within the MediaManager instance 151 152 (function(mediaManagerInstance, originalFunc, name, context){ 153 //NOTE need closure to "preserve" values of for-iteration 154 mediaManagerInstance.ctx[context][name] = function(){ 155 // console.log('executing '+context+'.'+name+', in context '+mediaManagerInstance,mediaManagerInstance);//DEBUG 156 return originalFunc.apply(mediaManagerInstance, arguments); 157 }; 158 })(instance, func, p, execId); 159 160 } 161 else { 162 //for non-functions: just attach to the new "sub-context" 163 instance.ctx[execId][p] = func; 164 } 165 166 } else { 167 168 //if there already is a function/property for this in the execution-context, 169 // print out an error: 170 171 logger.error('MediaManager', 'loadPlugin', 172 'cannot load implemantion for '+p+' of plugin "'+filePath+ 173 '" into execution-context "'+execId+ 174 '": context already exists!' 175 ); 176 177 } 178 179 180 }//END if(exportedFunctions<own>) 181 182 }//END for(p in exprotedFunctions) 183 184 185 }//END if(execId) 186 else { 187 jQuery.extend(true,instance,exportedFunctions); 188 newMediaPlugin = null; 189 } 190 191 if (successCallback) successCallback(); 192 193 }, instance, execId); 194 195 //"delete" global var for media plugin after loading 196 // TODO remove when/if other loading mechanism is established 197 // newMediaPlugin = void(0); 198 delete newMediaPlugin; 199 200 } 201 else { 202 console.error('Error loading MediaPlugin '+filePath + ' - no newMediaPlugin set!'); 203 if (failureCallback) failureCallback(); 204 } 205 }); 206 207 208 //DISABLED @russa: currently disabled, since debugging eval'ed code is problematic 209 // NOTE support for code-naming feature (see below) is currently somewhat broken in FireFox (e.g. location in error-stack is not done correctly) 210 // //NOTE: this new loading-mechanism avoids global VARIABLES by 211 // // * loading the script as text 212 // // * evaluating the script-text (i.e. executing the JavaScript) within an local context 213 // // * uses code-naming feature for eval'ed code: //@ sourceURL=... 214 // //i.e. eval(..) is used ... 215 // var targetPath = constants.getMediaPluginPath()+filePath; 216 // $.ajax({ 217 // async: true, 218 // dataType: "text", 219 // url: targetPath, 220 // success: function(data){ 221 // 222 // //add "dummy-export-code" to script-text 223 // // -> for "retrieving" the media-plugin implementation as return value from eval(..) 224 // var LOAD_MODULE_TEMPLATE_POSTFIX = 'var dummy = newMediaPlugin; dummy'; 225 // //use eval code naming feature... 226 // var codeId = ' sourceURL=' + constants.getMediaPluginPath()+filePath + '\n'; 227 // //... for WebKit: 228 // var CODE_ID_EXPR1 = '//@'; 229 // // ... and for FireFox: 230 // var CODE_ID_EXPR2 = '//#'; 231 // 232 // var newMediaPlugin = eval(data 233 // + CODE_ID_EXPR1 + codeId 234 // + CODE_ID_EXPR2 + codeId 235 // + LOAD_MODULE_TEMPLATE_POSTFIX 236 // ); 237 // 238 // if (typeof newMediaPlugin !== 'undefined' && newMediaPlugin){ 239 // newMediaPlugin.initialize(function(exportedFunctions){ 240 // jQuery.extend(true,instance,exportedFunctions); 241 // newMediaPlugin = null; 242 // if (successCallback) successCallback(); 243 // }, instance); 244 // } 245 // else { 246 // console.error('Error loading MediaPlugin '+filePath + ' - no newMediaPlugin set!'); 247 // if (failureCallback) failureCallback(); 248 // } 249 // } 250 // }).fail(function(jqxhr, settings, err){ 251 // // print out an error message 252 // var errMsg = err && err.stack? err.stack : err; 253 // console.error("[" + settings + "] " + JSON.stringify(jqxhr) + " -- " + partial.path + ": "+errMsg); //failure 254 // }); 255 }catch (e){ 256 console.error('Error loading MediaPlugin '+filePath+': '+e); 257 if (failureCallback) failureCallback(); 258 } 259 260 }; 261 262 // /** 263 // * "Register" a media-module implementation to the MediaManager. 264 // * 265 // * @param {MediaPlugin} newMediaPlugin 266 // * The new media-plugin which must have the 267 // * function <code>initialize</code>: 268 // * The initializer function will be called with 3 arguments: 269 // * (callbackFuntion(mediaPlugin: Object), instance: MediaManager, execId: String) 270 // * 271 // * the first argument (this callback-function from the MediaManager) 272 // * should be invoked by the media-plugin when it has it finished 273 // * initializing in its <code>initializeFunc</code>. 274 // * The callback must be invoked with on argument: 275 // * (mediaPlugin: Object) 276 // * where mediaPlugin is an object with all the functions and properties, 277 // * that the media-plugin exports to the MediaManager. 278 // * 279 // * @private 280 // * @function 281 // * 282 // * @memberOf MediaManager# 283 // */ 284 // function registerMediaPlugin(newMediaPlugin, successCallback, failureCallback, execId){ 285 // TODO move code from loadPlugin here: 286 // * export this as MediaManager.registerPlugin 287 // * media-plugins should call registerPlugin on MediaManager (instead of creating object newMediaPlugin) 288 // * open problem: how can success-callback for MediaManager-initialization be handled this way? (should be called after all plugins have themselves initialized) 289 // } 290 291 /** 292 * @constructs MediaManager 293 * @memberOf MediaManager.prototype 294 * @private 295 * @ignore 296 */ 297 function constructor(){ 298 299 /** 300 * map of listeners: 301 * event(String) -> listener(Function) 302 * 303 * @private 304 * @memberOf MediaManager.prototype 305 */ 306 var listener = new Dictionary(); 307 308 /** 309 * map of listener-observers: 310 * observers get notified if a listener for event X gets added/removed 311 * 312 * @private 313 * @memberOf MediaManager.prototype 314 */ 315 var listenerObserver = new Dictionary(); 316 317 /** 318 * exported as addListener() and on() 319 * 320 * @private 321 * @memberOf MediaManager.prototype 322 */ 323 var addListenerImpl = function(eventName, eventHandler){ 324 var list = listener.get(eventName); 325 if(!list){ 326 list = [eventHandler]; 327 listener.put(eventName, list); 328 } 329 else { 330 list.push(eventHandler); 331 } 332 333 //notify listener-observers for this event-type 334 this._notifyObservers(eventName, 'added', eventHandler); 335 }; 336 /** 337 * exported as removeListener() and off() 338 * 339 * @private 340 * @memberOf MediaManager.prototype 341 */ 342 var removeListenerImpl = function(eventName, eventHandler){ 343 var isRemoved = false; 344 var list = listener.get(eventName); 345 if(list){ 346 var size = list.length; 347 for(var i = size - 1; i >= 0; --i){ 348 if(list[i] === eventHandler){ 349 350 //move all handlers after i by 1 index-position ahead: 351 for(var j = size - 1; j > i; --j){ 352 list[j-1] = list[j]; 353 } 354 //remove last array-element 355 list.splice(size-1, 1); 356 357 //notify listener-observers for this event-type 358 this._notifyObservers(eventName, 'removed', eventHandler); 359 360 isRemoved = true; 361 break; 362 } 363 } 364 } 365 return isRemoved; 366 }; 367 368 369 /** 370 * The logger for the MediaManager. 371 * 372 * Exported as <code>_log</code> by the MediaManager instance. 373 * 374 * @private 375 * @memberOf MediaManager.prototype 376 */ 377 var logger = Logger.create(module);//initialize with requirejs-module information 378 379 380 /** 381 * Default execution context for functions: 382 * 383 * if not <code>falsy</code>, then functions will be executed in this context by default. 384 * 385 * @private 386 * @type String 387 * @memberOf MediaManager.prototype 388 */ 389 var defaultExecId = void(0); 390 391 /** @lends mmir.MediaManager.prototype */ 392 return { 393 394 /** 395 * A logger for the MediaManager and its plugins/modules. 396 * 397 * <p> 398 * This logger MAY be used by media-plugins and / or tools and helpers 399 * related to the MediaManager. 400 * 401 * <p> 402 * This logger SHOULD NOT be used by "code" that non-related to the 403 * MediaManager 404 * 405 * @name _log 406 * @type mmir.Logger 407 * @default mmir.Logger (logger instance for mmir.MediaManager) 408 * @public 409 * 410 * @memberOf mmir.MediaManager# 411 */ 412 _log: logger, 413 414 /** 415 * Execution context for plugins 416 * 417 * TODO add doc 418 * 419 * @name ctx 420 * @type mmir.Logger 421 * @default Object (empty context, i.e. plugins are loaded into the "root context", and no plugins loaded into the execution context) 422 * @public 423 * 424 * @memberOf mmir.MediaManager# 425 */ 426 ctx: {}, 427 428 /** 429 * Wait indicator, e.g. for speech input: 430 * <p> 431 * provides 2 functions:<br> 432 * 433 * <code>preparing()</code>: if called, the implementation indicates that the "user should wait"<br> 434 * <code>ready()</code>: if called, the implementation stops indicating that the "user should wait" (i.e. that the system is ready for user input now)<br> 435 * 436 * <p> 437 * If not set (or functions are not available) will do nothing 438 * 439 * @type mmir.env.media.IWaitReadyIndicator 440 * @memberOf mmir.MediaManager# 441 * 442 * @default Object (no implementation set) 443 * 444 * @see #_preparing 445 * @see #_ready 446 * 447 * @example 448 * //define custom wait/ready implementation: 449 * var impl = { 450 * preparing: function(str){ 451 * console.log('Media module '+str+' is preparing...'); 452 * }, 453 * ready: function(str){ 454 * console.log('Media module '+str+' is ready now!'); 455 * } 456 * }; 457 * 458 * //configure MediaManager to use custom implementation: 459 * mmir.MediaManager.waitReadyImpl = impl; 460 * 461 * //-> now plugins that call mmir.MediaManager._preparing() and mmir.MediaManager._ready() 462 * // will invoke the custom implementation's functions. 463 */ 464 waitReadyImpl: {}, 465 466 //TODO add API documentation 467 468 //... these are the standard audioInput procedures, that should be implemented by a loaded file 469 470 ///////////////////////////// audio input API: ///////////////////////////// 471 /** 472 * Start speech recognition with <em>end-of-speech</em> detection: 473 * 474 * the recognizer automatically tries to detect when speech has finished and then 475 * triggers the callback with the result. 476 * 477 * @async 478 * 479 * @param {Function} [successCallBack] OPTIONAL 480 * callback function that is triggered when a text result is available. 481 * The callback signature is: 482 * <code>callback(textResult)</code> 483 * @param {Function} [failureCallBack] OPTIONAL 484 * callback function that is triggered when an error occurred. 485 * The callback signature is: 486 * <code>callback(error)</code> 487 */ 488 recognize: function(successCallBack, failureCallBack){ 489 if(failureCallBack){ 490 failureCallBack("Audio Input: Speech Recognition is not supported."); 491 } 492 else { 493 console.error("Audio Input: Speech Recognition is not supported."); 494 } 495 }, 496 /** 497 * Start continuous speech recognition: 498 * 499 * The recognizer continues until {@link #stopRecord} is called. 500 * 501 * <p> 502 * If <code>isWithIntermediateResults</code> is used, the recognizer may 503 * invoke the callback with intermediate recognition results. 504 * 505 * TODO specify whether stopRecord should return the "gathered" intermediate results, or just the last one 506 * 507 * NOTE that not all implementation may support this feature. 508 * 509 * @async 510 * 511 * @param {Function} [successCallBack] OPTIONAL 512 * callback function that is triggered when a text result is available. 513 * The callback signature is: 514 * <code>callback(textResult)</code> 515 * @param {Function} [failureCallBack] OPTIONAL 516 * callback function that is triggered when an error occurred. 517 * The callback signature is: 518 * <code>callback(error)</code> 519 * @param {Boolean} [isWithIntermediateResults] OPTIONAL 520 * if <code>true</code>, the recognizer will return intermediate results 521 * by invoking the successCallback 522 * 523 * @see #stopRecord 524 */ 525 startRecord: function(successCallBack,failureCallBack, isWithIntermediateResults){ 526 if(failureCallBack){ 527 failureCallBack("Audio Input: Speech Recognition (recording) is not supported."); 528 } 529 else { 530 console.error("Audio Input: Speech Recognition (recording) is not supported."); 531 } 532 }, 533 /** 534 * Stops continuous speech recognition: 535 * 536 * After {@link #startRecord} was called, invoking this function will stop the recognition 537 * process and return the result by invoking the <code>succesCallback</code>. 538 * 539 * TODO specify whether stopRecord should return the "gathered" intermediate results, or just the last one 540 * 541 * @async 542 * 543 * @param {Function} [successCallBack] OPTIONAL 544 * callback function that is triggered when a text result is available. 545 * The callback signature is: 546 * <code>callback(textResult)</code> 547 * @param {Function} [failureCallBack] OPTIONAL 548 * callback function that is triggered when an error occurred. 549 * The callback signature is: 550 * <code>callback(error)</code> 551 * 552 * @see #startRecord 553 */ 554 stopRecord: function(successCallBack,failureCallBack){ 555 if(failureCallBack){ 556 failureCallBack("Audio Input: Speech Recognition (recording) is not supported."); 557 } 558 else { 559 console.error("Audio Input: Speech Recognition (recording) is not supported."); 560 } 561 }, 562 563 /** 564 * Cancel currently active speech recognition. 565 * 566 * Has no effect, if no recognition is active. 567 */ 568 cancelRecognition: function(successCallBack,failureCallBack){ 569 if(failureCallBack){ 570 failureCallBack("Audio Output: canceling Recognize Speech is not supported."); 571 } 572 else { 573 console.error("Audio Output: canceling Recognize Speech is not supported."); 574 } 575 }, 576 ///////////////////////////// audio output API: ///////////////////////////// 577 578 /** 579 * Play PCM audio data. 580 */ 581 playWAV: function(blob, onPlayedCallback, failureCallBack){ 582 if(failureCallBack){ 583 failureCallBack("Audio Output: play WAV audio is not supported."); 584 } 585 else { 586 console.error("Audio Output: play WAV audio is not supported."); 587 } 588 }, 589 /** 590 * Play audio file from the specified URL. 591 */ 592 playURL: function(url, onPlayedCallback, failureCallBack){ 593 if(failureCallBack){ 594 failureCallBack("Audio Output: play audio from URL is not supported."); 595 } 596 else { 597 console.error("Audio Output: play audio from URL is not supported."); 598 } 599 }, 600 /** 601 * Get an audio object for the audio file specified by URL. 602 * 603 * The audio object exports the following functions: 604 * 605 * <pre> 606 * play() 607 * stop() 608 * release() 609 * enable() 610 * disable() 611 * setVolume(number) 612 * getDuration() 613 * isPaused() 614 * isEnabled() 615 * </pre> 616 * 617 * NOTE: the audio object should only be used, after the <code>onLoadedCallback</code> 618 * was triggered. 619 * 620 * @param {String} url 621 * @param {Function} [onPlayedCallback] OPTIONAL 622 * @param {Function} [failureCallBack] OPTIONAL 623 * @param {Function} [onLoadedCallBack] OPTIONAL 624 * 625 * @returns {mmir.env.media.IAudio} the audio 626 * 627 * @see {mmir.env.media.IAudio#_constructor} 628 */ 629 getURLAsAudio: function(url, onPlayedCallback, failureCallBack, onLoadedCallBack){ 630 if(failureCallBack){ 631 failureCallBack("Audio Output: create audio from URL is not supported."); 632 } 633 else { 634 console.error("Audio Output: create audio from URL is not supported."); 635 } 636 }, 637 /** 638 * Get an empty audio object. This can be used as dummy or placeholder 639 * for a "real" audio object. 640 * 641 * The audio object exports the following functions: 642 * 643 * <pre> 644 * play() 645 * stop() 646 * release() 647 * enable() 648 * disable() 649 * setVolume(number) 650 * getDuration() 651 * isPaused() 652 * isEnabled() 653 * </pre> 654 * 655 * Note: 656 * 657 * <code>enable()</code> and <code>disable()</code> will set the internal 658 * enabled-state, which can be queried via <code>isEnabled()</code>. 659 * 660 * <code>play()</code> and <code>stop()</code> will set the internal 661 * playing-state, which can be queried via <code>isPaused()</code> 662 * (note however, that this empty audio does not actually play anything. 663 * 664 * <code>setVolume()</code> sets the internal volume-value. 665 * 666 * <code>getDuration()</code> will always return <code>0</code>. 667 * 668 * 669 * @returns {mmir.env.media.IAudio} the audio 670 * 671 * @see {mmir.env.media.IAudio#_constructor} 672 */ 673 createEmptyAudio: function(){ 674 return { 675 _enabled: true, 676 _play: false, 677 _volume: 1, 678 play: function(){ this._play = true; }, 679 stop: function(){ this._play = true; }, 680 enable: function(){ this._enabled = true; }, 681 disable: function(){ this._enabled = false; }, 682 release: function(){ this._enabled = false; }, 683 setVolume: function(vol){ this._volume = vol; }, 684 getDuration: function(){ return 0; }, 685 isPaused: function(){ return !this._play; }, 686 isEnabled: function(){ return this._enabled; } 687 }; 688 }, 689 ///////////////////////////// text-to-speech API: ///////////////////////////// 690 691 /** 692 * Synthesizes ("read out loud") text. 693 * 694 * @param {String|Array<String>|PlainObject} parameter 695 * if <code>String</code> or <code>Array</code> of <code>String</code>s 696 * synthesizes the text of the String, for an Array: each entry is interpreted as "sentence"; 697 * after each sentence, a short pause is inserted before synthesizing the 698 * the next sentence<br> 699 * for a <code>PlainObject</code>, the following properties should be used: 700 * <pre>{ 701 * text: string OR string Array, text that should be read aloud 702 * , pauseLength: OPTIONAL Length of the pauses between sentences in milliseconds 703 * , forceSingleSentence: OPTIONAL boolean, if true, a string Array will be turned into a single string 704 * , split: OPTIONAL boolean, if true and the text is a single string, it will be split using a splitter function 705 * , splitter: OPTIONAL function, replaces the default splitter-function. It takes a simple string as input and gives a string Array as output 706 * }</pre> 707 */ 708 textToSpeech: function(parameter, onPlayedCallback, failureCallBack){ 709 if(failureCallBack){ 710 failureCallBack("Audio Output: Text To Speech is not supported."); 711 } 712 else { 713 console.error("Audio Output: Text To Speech is not supported."); 714 } 715 }, 716 /** 717 * Cancel current synthesis. 718 */ 719 cancelSpeech: function(successCallBack,failureCallBack){ 720 if(failureCallBack){ 721 failureCallBack("Audio Output: canceling Text To Speech is not supported."); 722 } 723 else { 724 console.error("Audio Output: canceling Text To Speech is not supported."); 725 } 726 }, 727 728 ///////////////////////////// ADDITIONAL (optional) functions: ///////////////////////////// 729 /** 730 * Set the volume for the speech synthesis (text-to-speech). 731 * 732 * @param {Number} newValue 733 * TODO specify format / range 734 */ 735 setTextToSpeechVolume: function(newValue){ 736 console.error("Audio Output: set volume for Text To Speech is not supported."); 737 } 738 739 740 ///////////////////////////// MediaManager "managing" functions: ///////////////////////////// 741 /** 742 * Adds the handler-function for the event. 743 * 744 * This function calls {@link #_notifyObservers} for the eventName with 745 * <code>actionType "added"</code>. 746 * 747 * 748 * Event names (and firing events) are specific to the loaded media plugins. 749 * 750 * TODO list events that the default media-plugins support 751 * * "miclevelchanged": fired by AudioInput plugins that support querying the microphone (audio input) levels 752 * 753 * A plugin can tigger / fire events using the helper {@link #_fireEvent} 754 * of the MediaManager. 755 * 756 * 757 * Media plugins may observe registration / removal of listeners 758 * via {@link #_addListenerObserver} and {@link #_removeListenerObserver}. 759 * Or get and iterate over listeners via {@link #getListeners}. 760 * 761 * 762 * 763 * 764 * @param {String} eventName 765 * @param {Function} eventHandler 766 * 767 * @function 768 */ 769 , addListener: addListenerImpl 770 /** 771 * Removes the handler-function for the event. 772 * 773 * Calls {@link #_notifyObservers} for the eventName with 774 * <code>actionType "removed"</code>, if the handler 775 * was actually removed. 776 * 777 * @param {String} eventName 778 * @param {Function} eventHandler 779 * 780 * @returns {Boolean} 781 * <code>true</code> if the handler function was actually 782 * removed, and <code>false</code> otherwise. 783 * 784 * @function 785 */ 786 , removeListener: removeListenerImpl 787 /** 788 * @function 789 * @see #addListener 790 */ 791 , on:addListenerImpl 792 /** 793 * @function 794 * @see #removeListener 795 */ 796 , off: removeListenerImpl 797 /** 798 * Get list of registered listeners / handlers for an event. 799 * 800 * @returns {Array<Function>} of event-handlers. 801 * Empty, if there are no event handlers for eventName 802 */ 803 , getListeners: function(eventName){ 804 var list = listener.get(eventName); 805 if(list && list.length){ 806 //return copy of listener-list 807 return list.slice(0,list.length); 808 } 809 return []; 810 } 811 /** 812 * Check if at least one listener / handler is registered for the event. 813 * 814 * @returns {Boolean} <code>true</code> if at least 1 handler is registered 815 * for eventName; otherwise <code>false</code>. 816 */ 817 , hasListeners: function(eventName){ 818 var list = listener.get(eventName); 819 return list && list.length > 0; 820 } 821 /** 822 * Helper for firing / triggering an event. 823 * This should only be used by media plugins (that handle the eventName). 824 * 825 * @param {String} eventName 826 * @param {Array} argsArray 827 * the list of arguments with which the event-handlers 828 * will be called. 829 * @protected 830 */ 831 , _fireEvent: function(eventName, argsArray){ 832 var list = listener.get(eventName); 833 if(list && list.length){ 834 for(var i=0, size = list.length; i < size; ++i){ 835 list[i].apply(this, argsArray); 836 } 837 } 838 } 839 /** 840 * Helper for notifying listener-observers about changes (adding/removing listeners). 841 * This should only be used by media plugins (that handle the eventName). 842 * 843 * @param {String} eventName 844 * @param {String} actionType 845 * the change-type that occurred for the event/event-handler: 846 * one of <code>["added" | "removed"]</code>. 847 * @param {Function} eventHandler 848 * the event-handler function that has changed. 849 * 850 * @protected 851 */ 852 , _notifyObservers: function(eventName, actionType, eventHandler){//actionType: one of "added" | "removed" 853 var list = listenerObserver.get(eventName); 854 if(list && list.length){ 855 for(var i=0, size = list.length; i < size; ++i){ 856 list[i](actionType,eventHandler); 857 } 858 } 859 } 860 /** 861 * Add an observer for registration / removal of event-handler. 862 * 863 * The observer gets notified,when handlers are registered / removed for the event. 864 * 865 * The observer-callback function will be called with the following 866 * arguments 867 * 868 * <code>(eventName, ACTION_TYPE, eventHandler)</code> 869 * where 870 * <ul> 871 * <li>eventName: String the name of the event that should be observed</li> 872 * <li>ACTION_TYPE: the type of action: "added" if the handler was 873 * registered for the event, "removed" if the the handler was removed 874 * </li> 875 * <li>eventHandler: the handler function that was registered or removed</li> 876 * </ul> 877 * 878 * @param {String} eventName 879 * @param {Function} observerCallback 880 */ 881 , _addListenerObserver: function(eventName, observerCallback){ 882 var list = listenerObserver.get(eventName); 883 if(!list){ 884 list = [observerCallback]; 885 listenerObserver.put(eventName, list); 886 } 887 else { 888 list.push(observerCallback); 889 } 890 } 891 892 , _removeListenerObserver: function(eventName, observerCallback){ 893 var isRemoved = false; 894 var list = listenerObserver.get(eventName); 895 if(list){ 896 var size = list.length; 897 for(var i = size - 1; i >= 0; --i){ 898 if(list[i] === observerCallback){ 899 900 //move all handlers after i by 1 index-position ahead: 901 for(var j = size - 1; j > i; --j){ 902 list[j-1] = list[j]; 903 } 904 //remove last array-element 905 list.splice(size-1, 1); 906 907 isRemoved = true; 908 break; 909 } 910 } 911 } 912 return isRemoved; 913 } 914 /** 915 * Executes function <code>funcName</code> in "sub-module" <code>ctx</code> 916 * with arguments <code>args</code>. 917 * 918 * <p> 919 * If there is no <code>funcName</code> in "sub-module" <code>ctx</code>, 920 * then <code>funcName</code> from the "main-module" (i.e. from the MediaManager 921 * instance itself) will be used. 922 * 923 * @param {String} ctx 924 * the execution context, i.e. "sub-module", in which to execute funcName.<br> 925 * If <code>falsy</code>, the "root-module" will used as execution context. 926 * @param {String} funcName 927 * the function name 928 * @param {Array} args 929 * the arguments for function "packaged" in an array 930 * 931 * @throws {ReferenceError} 932 * if <code>funcName</code> does not exist in the requested Execution context.<br> 933 * Or if <code>ctx</code> is not <code>falsy</code> but there is no valid execution 934 * context <code>ctx</code> in MediaManager. 935 * 936 * @example 937 * 938 * //same as mmir.MediaManager.ctx.android.textToSpeech("...", function...): 939 * mmir.MediaManager.perform("android", "textToSpeech", ["some text to read out loud", 940 * function onFinished(){ console.log("finished reading."); } 941 * ]); 942 * 943 * //same as mmir.MediaManager.textToSpeech("...", function...) 944 * //... IF the defaultExecId is falsy 945 * // (i.e. un-changed or set to falsy value via setDefaultExec()) 946 * mmir.MediaManager.perform(null, "textToSpeech", ["some text to read out loud", 947 * function onFinished(){ console.log("finished reading."); } 948 * ]); 949 * 950 */ 951 , perform: function(ctx, funcName, args){ 952 953 var func; 954 955 if(!ctx){ 956 957 if(defaultExecId && typeof this.ctx[defaultExecId][funcName] !== 'undefined'){ 958 func = this.ctx[defaultExecId][funcName]; 959 } 960 961 962 } 963 else if(ctx && typeof this.ctx[ctx] !== 'undefined') { 964 965 if(typeof this.ctx[ctx][funcName] !== 'undefined') { 966 func = this.ctx[ctx][funcName]; 967 } 968 969 } else { 970 throw new ReferenceError('There is no context for "'+ctx+'" in MediaManager.ctx!');///////////////////////////// EARLY EXIT //////////////////// 971 } 972 973 974 if(!func){ 975 func = this[funcName]; 976 } 977 978 979 if(typeof func === 'undefined'){ 980 throw new ReferenceError('There is no function '+funcName+' in MediaManager'+(ctx? ' context ' + ctx : (defaultExecId? ' default context ' + defaultExecId : '')) + '!');///////////////////////////// EARLY EXIT //////////////////// 981 } 982 983 return func.apply(this, args); 984 } 985 /** 986 * Returns function <code>funcName</code> from "sub-module" <code>ctx</code>. 987 * 988 * <p> 989 * If there is no <code>funcName</code> in "sub-module" <code>ctx</code>, 990 * then <code>funcName</code> from the "main-module" (i.e. from the MediaManager 991 * instance itself) will be returned. 992 * 993 * <p> 994 * NOTE that the returned functions will always execute within the context of the 995 * MediaManager instance (i.e. <code>this</code> will refer to the MediaManager instance). 996 * 997 * 998 * @param {String} ctx 999 * the execution context, i.e. "sub-module", in which to execute funcName.<br> 1000 * If <code>falsy</code>, the "root-module" will used as execution context. 1001 * @param {String} funcName 1002 * the function name 1003 * 1004 * @throws {ReferenceError} 1005 * if <code>funcName</code> does not exist in the requested Execution context.<br> 1006 * Or if <code>ctx</code> is not <code>falsy</code> but there is no valid execution 1007 * context <code>ctx</code> in MediaManager. 1008 * 1009 * @example 1010 * 1011 * //same as mmir.MediaManager.ctx.android.textToSpeech("...", function...): 1012 * mmir.MediaManager.getFunc("android", "textToSpeech")("some text to read out loud", 1013 * function onFinished(){ console.log("finished reading."); } 1014 * ); 1015 * 1016 * //same as mmir.MediaManager.textToSpeech("...", function...): 1017 * //... IF the defaultExecId is falsy 1018 * // (i.e. un-changed or set to falsy value via setDefaultExec()) 1019 * mmir.MediaManager.getFunc(null, "textToSpeech")("some text to read out loud", 1020 * function onFinished(){ console.log("finished reading."); } 1021 * ); 1022 * 1023 */ 1024 , getFunc: function(ctx, funcName){//this function performs worse for the "root execution" context, than perform(), since an additional wrapper function must be created 1025 1026 var isRoot = false; 1027 1028 if(!ctx){ 1029 1030 if(!defaultExecId){ 1031 isRoot = true; 1032 } 1033 else { 1034 if(typeof this.ctx[defaultExecId][funcName] !== 'undefined'){ 1035 return this.ctx[defaultExecId][funcName];/////////// EARLY EXIT ////////////////// 1036 } 1037 else { 1038 isRoot = true; 1039 } 1040 } 1041 } 1042 1043 if(ctx && typeof this.ctx[ctx] !== 'undefined'){ 1044 if(!isRoot && typeof this.ctx[ctx][funcName] !== 'undefined'){ 1045 return this.ctx[ctx][funcName];///////////////////////////// EARLY EXIT //////////////////// 1046 } 1047 } 1048 else { 1049 throw new ReferenceError('There is no context for "'+ctx+'" in MediaManager.ctx!');///////////////////////////// EARLY EXIT //////////////////// 1050 } 1051 1052 //-> return the implementation of the "root execution context" 1053 1054 if(typeof instance[funcName] === 'undefined'){ 1055 throw new ReferenceError('There is no function '+funcName+' in MediaManager'+(ctx? ' context ' + ctx : (defaultExecId? ' default context ' + defaultExecId : '')) + '!');///////////////////////////// EARLY EXIT //////////////////// 1056 } 1057 1058 //need to create proxy function, in order to preserve correct execution context 1059 // (i.e. the MediaManager instance) 1060 return function() { 1061 return instance[funcName].apply(instance, arguments); 1062 }; 1063 1064 }, 1065 /** 1066 * Set the default execution context. 1067 * 1068 * If not explicitly set, or set to a <code>falsy</code> value, 1069 * then the "root" execution context is the default context. 1070 * 1071 * @param {String} ctxId 1072 * the new default excution context for loaded media modules 1073 * (if <code>falsy</code> the default context will be the "root context") 1074 * 1075 * @throws {ReferenceError} 1076 * if <code>ctxId</code> is no valid context 1077 * 1078 * @example 1079 * 1080 * //if context "nuance" exists: 1081 * mmir.MediaManager.setDefaultCtx("nuance") 1082 * 1083 * // -> now the following calls are equal to mmir.MediaManager.ctx.nuance.textToSpeech("some text") 1084 * mmir.MediaManager.perform(null, "textToSpeech", ["some text"]); 1085 * mmir.MediaManager.getFunc(null, "textToSpeech")("some text"); 1086 * 1087 * //reset to root context: 1088 * mmir.MediaManager.setDefaultCtx("nuance"); 1089 * 1090 * // -> now the following call is equal to mmir.MediaManager.textToSpeech("some text") again 1091 * mmir.MediaManager.perform("textToSpeech", ["some text"]); 1092 * 1093 */ 1094 setDefaultCtx: function(ctxId){ 1095 if(ctxId && typeof instance.ctx[ctxId] === 'undefined'){ 1096 throw new ReferenceError('There is no context for "'+ctxId+'" in MediaManager.ctx!');///////////////////////////// EARLY EXIT //////////////////// 1097 } 1098 defaultExecId = ctxId; 1099 }, 1100 /** 1101 * This function is called by media plugin implementations (i.e. modules) 1102 * to indicate that they are preparing something and that the user should 1103 * wait. 1104 * 1105 * <p> 1106 * The actual implementation for <code>_preparing(String)</code> is given by 1107 * {@link #waitReadyImpl}.preparing (if not set, then calling <code>_preparing(String)</code> 1108 * will have no effect. 1109 * 1110 * @param {String} moduleName 1111 * the module name from which the function was invoked 1112 * 1113 * @function 1114 * @protected 1115 * 1116 * @see #waitReadyImpl 1117 * @see #_ready 1118 */ 1119 _preparing: function(moduleName){ 1120 if(this.waitReadyImpl && this.waitReadyImpl.preparing){ 1121 this.waitReadyImpl.preparing(moduleName); 1122 } 1123 }, 1124 /** 1125 * This function is called by media plugin implementations (i.e. modules) 1126 * to indicate that they are now ready and that the user can start interacting. 1127 * 1128 * <p> 1129 * The actual implementation for <code>_ready(String)</code> is given by the 1130 * {@link #waitReadyImpl} implementation (if not set, then calling <code>_ready(String)</code> 1131 * will have no effect. 1132 * 1133 * @param {String} moduleName 1134 * the module name from which the function was invoked 1135 * 1136 * @function 1137 * @protected 1138 * 1139 * @see #waitReadyImpl 1140 * @see #_ready 1141 */ 1142 _ready: function(moduleName){ 1143 if(this.waitReadyImpl && this.waitReadyImpl.ready){ 1144 this.waitReadyImpl.ready(moduleName); 1145 } 1146 } 1147 1148 };//END: return{... 1149 1150 };//END: constructor(){... 1151 1152 1153 //has 2 default configuarions: 1154 // if isCordovaEnvironment TRUE: use 'cordova' config 1155 // if FALSEy: use 'browser' config 1156 // 1157 // NOTE: this setting/paramater is overwritten, if the configuration has a property 'mediaPlugins' set!!! 1158 /** 1159 * HELPER for init-function: 1160 * determines, which plugins (i.e. files) should be loaded. 1161 * 1162 * <p> 1163 * has 2 default configuarions:<br> 1164 * if isCordovaEnvironment TRUE: use 'cordova' config<br> 1165 * if FALSEy: use 'browser' config 1166 * <p> 1167 * OR<br> 1168 * loads the list for the current environment (cordova or browser) that is set in configuration.json via <br> 1169 * <pre> 1170 * "mediaManager": { 1171 * "cordova": [...], 1172 * "browser": [...] 1173 * } 1174 * </pre> 1175 * 1176 * <p> 1177 * Each entry may either be a String (file name of the plugin) or an Object with 1178 * properties 1179 * <pre> 1180 * mod: <file name for the module> //String 1181 * ctx: <an ID for the module> //String 1182 * </pre> 1183 * 1184 * If <b>String</b>: the functions of the loaded plugin will be attached to the MediaManager instance: 1185 * <code>mmir.MediaManager.thefunction()</code> 1186 * <br> 1187 * If <b>{mod: plugin,ctx: theContextId}</b>: the functions of the loaded plugin will be attached to the "sub-module" 1188 * to the MediaManager instance <em>(NOTE the execution context of the function will remain within 1189 * the MediaManager instance, i.e. <code>this</code> will still refer to the MediaManager instance)</em>: 1190 * <code>mmir.MediaManager.theId.thefunction()</code> 1191 * 1192 * <p> 1193 * If plugins are loaded with an ID, you can use 1194 * <code>mmir.MediaManager.getFunc(ctxId, func)(the, arguments)</code> or 1195 * <code>mmir.MediaManager.perform(ctxId, func, [the, arguments])</code>: 1196 * If the "sub-module" ctxId does not have the function func (i.e. no MediaManager.ctx.ctxId.func exists), then the default function 1197 * in MediaManager will be executed (i.e. MediaManager.func(the, arguments) ). 1198 * 1199 * 1200 * @returns {Array<String>} 1201 * the list of plugins which should be loaded 1202 * 1203 * @private 1204 * @memberOf mmir.MediaManager# 1205 */ 1206 function getPluginsToLoad(configurationName){//if configurationName is omitted, then it is automatically detected 1207 1208 var env = configurationName; 1209 var pluginArray = []; 1210 1211 var dataFromConfig = configurationManager.get('mediaManager.plugins', true); 1212 1213 if(!env){ 1214 1215 var envSetting = constants.getEnv(); 1216 if(envSetting === 'cordova'){ 1217 1218 //try to find config for specific cordova-env 1219 envSetting = constants.getEnvPlatform(); 1220 if(envSetting !== 'default'){ 1221 1222 //if there is a config present for the specific envSetting, then use it: 1223 if((dataFromConfig && dataFromConfig[envSetting]) || _defaultPlugins[envSetting]){ 1224 //if there is a config present for the envSetting, then use it: 1225 env = envSetting; 1226 } 1227 1228 } 1229 1230 } else if(dataFromConfig && dataFromConfig[envSetting]){ 1231 //if there is a non-default config present for the envSetting, then use it 1232 // if there is a deault config, then the env will also be a default one 1233 // -> this will be detected by default-detection-mechanism below 1234 env = envSetting; 1235 } 1236 1237 //if there is no env value yet, use default criteria browser vs. cordova env: 1238 if(!env){ 1239 1240 var isCordovaEnvironment = ! constants.isBrowserEnv(); 1241 if (isCordovaEnvironment) { 1242 env = 'cordova'; 1243 } else { 1244 env = 'browser'; 1245 } 1246 } 1247 1248 //ASSERT env is non-empty String 1249 } 1250 1251 if (dataFromConfig && dataFromConfig[env]){ 1252 pluginArray = pluginArray.concat(dataFromConfig[env]); 1253 } else{ 1254 pluginArray = pluginArray.concat(_defaultPlugins[env]); 1255 } 1256 1257 return pluginArray; 1258 } 1259 /** 1260 * 1261 * @private 1262 * @memberOf mmir.MediaManager# 1263 */ 1264 function loadAllPlugins(pluginArray, successCallback,failureCallback){ 1265 1266 if (pluginArray == null || pluginArray.length<1){ 1267 if (successCallback) { 1268 successCallback(); 1269 } 1270 return; 1271 } 1272 1273 var ctxId; 1274 var newPluginName = pluginArray.pop(); 1275 if(newPluginName.ctx && newPluginName.mod){ 1276 ctxId = newPluginName.ctx; 1277 newPluginName = newPluginName.mod; 1278 } 1279 1280 loadPlugin(newPluginName, function (){ 1281 console.log(newPluginName+' loaded!'); 1282 loadAllPlugins(pluginArray,successCallback, failureCallback);}, 1283 failureCallback, 1284 ctxId 1285 ); 1286 } 1287 1288 1289 var _stub = { 1290 1291 /** @scope MediaManager.prototype */ 1292 1293 //TODO add for backwards compatibility?: 1294 // create : function(){ return this.init.apply(this, arguments); }, 1295 1296 /** 1297 * Object containing the instance of the class {{#crossLink "audioInput"}}{{/crossLink}} 1298 * 1299 * If <em>listenerList</em> is provided, each listener will be registered after the instance 1300 * is initialized, but before media-plugins (i.e. environment specfific implementations) are 1301 * loaded. 1302 * Each entry in the <em>listenerList</em> must have fields <tt>name</tt> (String) and 1303 * <tt>listener</tt> (Function), where 1304 * <br> 1305 * name: is the name of the event 1306 * <br> 1307 * listener: is the listener implementation (the signature/arguments of the listener function depends 1308 * on the specific event for which the listener will be registered) 1309 * 1310 * 1311 * @method init 1312 * @param {Function} [successCallback] OPTIONAL 1313 * callback that gets triggered after the MediaManager instance has been initialized. 1314 * @param {Function} [failureCallback] OPTIONAL 1315 * a failure callback that gets triggered if an error occurs during initialization. 1316 * @param {Array<Object>} [listenerList] OPTIONAL 1317 * a list of listeners that should be registered, where each entry is an Object 1318 * with properties: 1319 * <pre> 1320 * { 1321 * name: String the event name, 1322 * listener: Function the handler function 1323 * } 1324 * </pre> 1325 * @return {Object} 1326 * an Deferred object that gets resolved, after the {@link mmir.MediaManager} 1327 * has been initialized. 1328 * @public 1329 * 1330 * @memberOf mmir.MediaManager.prototype 1331 * 1332 */ 1333 init: function(successCallback, failureCallback, listenerList){ 1334 1335 var defer = jQuery.Deferred(); 1336 var deferredSuccess = function(){ 1337 defer.resolve(); 1338 }; 1339 var deferredFailure = function(){ 1340 defer.reject(); 1341 }; 1342 1343 1344 if(successCallback){ 1345 defer.done(successCallback); 1346 } 1347 1348 if(deferredFailure){ 1349 defer.fail(failureCallback); 1350 } 1351 1352 1353 if (instance === null) { 1354 jQuery.extend(true,this,constructor()); 1355 instance = this; 1356 1357 if(listenerList){ 1358 for(var i=0, size = listenerList.length; i < size; ++i){ 1359 instance.addListener(listenerList[i].name, listenerList[i].listener); 1360 } 1361 } 1362 1363 var pluginConfig = getPluginsToLoad(); 1364 loadAllPlugins(pluginConfig,deferredSuccess, deferredFailure); 1365 1366 } 1367 else if(listenerList){ 1368 for(var i=0, size = listenerList.length; i < size; ++i){ 1369 instance.addListener(listenerList[i].name, listenerList[i].listener); 1370 } 1371 } 1372 1373 return defer.promise(this); 1374 }, 1375 /** 1376 * Same as {@link #init}. 1377 * 1378 * @deprecated access MediaManger directly via <code>mmir.MediaManager.someFunction</code> - <em>&tl;internal: for initialization use <code>init()</code> instead></em> 1379 * 1380 * @function 1381 * @public 1382 * @memberOf mmir.MediaManager.prototype 1383 */ 1384 getInstance: function(){ 1385 return this.init(null, null); 1386 }, 1387 /** 1388 * loads a file. If the file implements a function initialize(f) 1389 * where the function f is called with a set of functions e, then those functions in e 1390 * are added to the visibility of audioInput, and will from now on be applicable by calling 1391 * mmir.MediaManager.<function name>(). 1392 * 1393 * @deprecated do not use. 1394 * @function 1395 * @protected 1396 * @memberOf mmir.MediaManager.prototype 1397 * 1398 */ 1399 loadFile: function(filePath,successCallback, failureCallback, execId){ 1400 if (instance=== null) { 1401 this.init(); 1402 } 1403 1404 loadPlugin(filePath,sucessCallback, failureCallback, execId); 1405 1406 } 1407 }; 1408 1409 return _stub; 1410 1411 });//END: define(..., function(){... 1412