Source: env/media/html5AudioOutput.js

/*
 * 	Copyright (C) 2012-2013 DFKI GmbH
 * 	Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
 * 	German Research Center for Artificial Intelligence
 * 	http://www.dfki.de
 * 
 * 	Permission is hereby granted, free of charge, to any person obtaining a 
 * 	copy of this software and associated documentation files (the 
 * 	"Software"), to deal in the Software without restriction, including 
 * 	without limitation the rights to use, copy, modify, merge, publish, 
 * 	distribute, sublicense, and/or sell copies of the Software, and to 
 * 	permit persons to whom the Software is furnished to do so, subject to 
 * 	the following conditions:
 * 
 * 	The above copyright notice and this permission notice shall be included 
 * 	in all copies or substantial portions of the Software.
 * 
 * 	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * 	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 * 	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * 	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * 	CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 * 	TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * 	SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


newMediaPlugin = {

		/**  @memberOf Html5AudioOutput# */
		initialize: function(callBack, mediaManager){
			
			/**  @memberOf Html5AudioOutput# */
			var _pluginName = 'html5AudioOutput';
			
			/** 
			 * legacy mode: use pre-v4 API of mmir-lib
			 * @memberOf Html5AudioOutput#
			 */
			var _isLegacyMode = true;
			/** 
			 * Reference to the mmir-lib core (only available in non-legacy mode)
			 * @type mmir
			 * @memberOf Html5AudioOutput#
			 */
			var _mmir = null;
			if(mediaManager._get_mmir){
				//_get_mmir() is only available for >= v4
				_mmir = mediaManager._get_mmir();
				//just to make sure: set legacy-mode if version is < v4
				_isLegacyMode = _mmir? _mmir.isVersion(4, '<') : true;
			}
			/**
			 * HELPER for require(): 
			 * 		use module IDs (and require instance) depending on legacy mode
			 * 
			 * @param {String} id
			 * 			the require() module ID
			 * 
			 * @returns {any} the require()'ed module
			 * 
			 * @memberOf Html5AudioOutput#
			 */
			var _req = function(id){
				var name = (_isLegacyMode? '' : 'mmirf/') + id;
				return _mmir? _mmir.require(name) : require(name);
			};
			
			/** 
			 * @type Function
			 * @memberOf Html5AudioOutput#
			 */
			var extend = _req('util/extend');
			
			/**
			 * Media error (codes):
			 * 
			 * the corresponding code is their <code>index</code>.
			 * 
			 * <p>
			 * 
			 * Code 0 is an internal error code for unknown/unspecific error causes.
			 * <br>
			 * Codes 1 - 4 correspond to the HTML5 MediaError interface
			 * (which are the same as Cordova's Audio MediaError codes).
			 * The description texts are taken from the HTML5 documentation.
			 * 
			 * @enum
			 * @constant
			 * @type Array<String>
			 * @memberOf Html5AudioOutput#
			 * 
			 * @see <a href="https://html.spec.whatwg.org/multipage/embedded-content.html#mediaerror">https://html.spec.whatwg.org/multipage/embedded-content.html#mediaerror</a>
			 * @see <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media">http://plugins.cordova.io/#/package/org.apache.cordova.media</a>
			 */
			var MediaError = [
			  	{name: 'MEDIA_ERR_UNKNOWN', 		code: 0, description: 'An unknown or unspecific (internal) error occurred.'},
			  	{name: 'MEDIA_ERR_ABORTED', 		code: 1, description: 'The fetching process for the media resource was aborted by the user agent at the user\'s request.'},
			  	{name: 'MEDIA_ERR_NETWORK', 		code: 2, description: 'A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable.'},
			  	{name: 'MEDIA_ERR_DECODE', 			code: 3, description: 'An error of some description occurred while decoding the media resource, after the resource was established to be usable.'},
			  	{name: 'MEDIA_ERR_NONE_SUPPORTED', 	code: 4, description: 'The media resource indicated by the src attribute or assigned media provider object was not suitable.'}
			];
			
			/**
			 * HELPER for creating error object that is returned in the failureCallbacks
			 * 
			 * @param {Number} code
			 * 			The error code: if in [1,4], the corresponding HTML5 MediaError information will be returned
			 * 			Otherwise an "error 0", i.e. internal/unknown error will be created
			 * @param {Event|Error} errorEvent
			 * 			the causing event- or error-object
			 * 
			 * @returns {Object} the error object, which has 3 properties: 
			 * 			  code (Number): the error code
			 * 			  message (String): the error name
			 * 			  description (String): a descriptive text for the error
			 * 
			 * @memberOf Html5AudioOutput#
			 */
			function createError(code, errorEvent){
				
				var mErr;
				if(code > 0 && code < 5){
					mErr = MediaError[code];
				}
				else {
					mErr = MediaError[0];
				}
				
				return {
						code: 			mErr.code,
						message: 		mErr.name,
						description: 	mErr.description + (code===0 && errorEvent? ' ' + errorEvent.toString() : '')
				};
			}
			
			/**
			 * FACTORY for creating error-listeners (that trigger the failureCallback)
			 * 
			 * @param {Object} [ctx]
			 * 			the context for the errorCallback.
			 * 			IF omitted, the callback will be within the default (i.e. global) context.
			 * @param {Function} [errorCallback]
			 * 			the error callback.
			 * 			Is invoked with 2 arguments:
			 * 			errorCallback(error, event)
			 * 			where error has 3 properties: 
			 * 			  code (Number): the error code
			 * 			  message (String): the error name
			 * 			  description (String): a descriptive text for the error
			 * 
			 * 			IF omitted, the error will be printed to the console.
			 * 
			 * @return {Function} wrapper function that can be registered as event-listener (takes one argument: the event)
			 * 
			 * @memberOf Html5AudioOutput#
			 */
			function createErrorWrapper(ctx, errorCallback){
				
				return function(evt){
					
					var code;
					//extract MediaError from event's (audio) target:
					if(evt && evt.target && evt.target.error && (code = evt.target.error.code) && code > 0 && code < 5){
//						code = code; //NO-OP: code value was already assigned in IF clause
					}
					else {
						//unknown cause: create internal-error object
						code = 0;
					}
					
					var err = createError(code, evt);
					
					if(errorCallback){
						errorCallback.call(ctx, err, evt);
					}
					else {
						console.error(err.message + ' (code '+err.code + '): '+err.description, evt);
					}
				};
			}
			
			/**
			 * HELPER for creating data-URL from binary data (blob)
			 * 
			 * @param {Blob} blob
			 * 			The audio data as blob
			 * @param {Function} callback
			 * 			callback that will be invoked with the data-URL:
			 * 			<code>callback(dataUrl)</code>
			 * 
			 * @memberOf Html5AudioOutput#
			 */
			function createDataUrl(blob, callback){
				
				if(window.URL){
					callback( window.URL.createObjectURL(blob) );
				}
				else if(window.webkitURL){
					callback( window.webkitURL.createObjectURL(blob) );
				}
				else {
					
					//DEFAULT: use file-reader:
					var fileReader = new FileReader();

		            // onload needed since Google Chrome doesn't support addEventListener for FileReader
		            fileReader.onload = function (evt) {
		            	// Read out "file contents" as a Data URL
		                var dataUrl = evt.target.result;
		                callback(dataUrl);
		            };
		            
		            //start loading the blob as Data URL:
		            fileReader.readAsDataURL(blob);
				}
				
			}

			//invoke the passed-in initializer-callback and export the public functions:
			callBack({
				/**
				 * @public
				 * @memberOf Html5AudioOutput.prototype
				 * @see mmir.MediaManager#playWAV
				 */
				playWAV: function(blob, onEnd, failureCallback, successCallback){
					
					try {
						
						var self = this;
						createDataUrl(blob, function(dataUrl){
							
							self.playURL(dataUrl, onEnd, failureCallback, successCallback);
							
						});
						
						
					} catch (e){
						
						var err = createError(0,e);
						if(failureCallback){
							failureCallback.call(null, err, e);
						}
						else {
							console.error(err.message + ': ' + err.description, e);
						}
					}
				},
				/**
				 * @public
				 * @memberOf Html5AudioOutput.prototype
				 * @see mmir.MediaManager#playURL
				 */
				playURL: function(url, onEnd, failureCallback, successCallback){
					
					try {
						
						var audio = new Audio(url);

						if(failureCallback){
							audio.addEventListener('error', createErrorWrapper(audio, failureCallback), false);
						}
						
						if(onEnd){
							audio.addEventListener('ended', onEnd, false);
						}
						
						if(successCallback){
							audio.addEventListener('canplay', successCallback, false);
						}
						
						audio.play();
						
					} catch (e){
						
						var err = createError(0,e);
						if(failureCallback){
							failureCallback.call(null, err, e);
						}
						else {
							console.error(err.message + ': ' + err.description, e);
						}
					}
					
				},
				/**
				 * @public
				 * @type Function
				 * @memberOf CordovaAudioOutput.prototype
				 * @see mmir.MediaManager#play
				 */
				play: mediaManager.play,
				
				/**
				 * @public
				 * @memberOf Html5AudioOutput.prototype
				 * @see mmir.MediaManager#getWAVAsAudio
				 */
				getWAVAsAudio: function(blob, callback, onEnd, failureCallback, onInit, emptyAudioObj){
					
					if(!emptyAudioObj){
						emptyAudioObj = mediaManager.createEmptyAudio();
					}
					
					try {
						
						var self = this;
						
						createDataUrl(blob, function(dataUrl){
							
							var audioObj;

							//do not start creating the blob, if the audio was already discarded:
							if(emptyAudioObj.isEnabled()){
								audioObj = self.getURLAsAudio(dataUrl, onEnd, failureCallback, onInit, emptyAudioObj);
							} else {
								audioObj = emptyAudioObj;
							}
							
							if(callback){
								callback.call(audioObj, audioObj);
							}
							
						});
						
						
					} catch (e){
						
						var err = createError(0, e);
						if(failureCallback){
							failureCallback.call(emptyAudioObj, err, e);
						}
						else {
							console.error(err.message + ': ' + err.description, e);
						}
					}
					
					return emptyAudioObj;
				},
				
				/**
				 * @public
				 * @memberOf Html5AudioOutput.prototype
				 * @see mmir.MediaManager#getURLAsAudio
				 */
				getURLAsAudio: function(url, onEnd, failureCallback, successCallback, audioObj){
					
					try {

						/**
						 * @private
						 * @memberOf AudioHtml5Impl#
						 */
						var enabled = audioObj? audioObj._enabled : true;
						/**
						 * @private
						 * @memberOf AudioHtml5Impl#
						 */
						var ready = false;
						/**
						 * @private
						 * @memberOf AudioHtml5Impl#
						 */
						var my_media = new Audio(url);

						/**
						 * @private
						 * @memberOf AudioHtml5Impl#
						 */
						var canPlayCallback = function(){
							ready = true;
//							console.log("sound is ready!");

							//FIX: remove this listener after first invocation 
							//     (this is meant as "on-init" listener, but "canplay" 
							//      may be triggered multiple times during the lifetime of the audio object).
							this.removeEventListener('canplay', canPlayCallback);
							canPlayCallback = null;

							if (enabled && successCallback){
								successCallback.apply(mediaImpl, arguments);
							}
						};
						my_media.addEventListener('canplay', canPlayCallback, false);
						
						/**
						 * The Audio abstraction that is returned by {@link mmir.MediaManager#getURLAsAudio}.
						 * 
						 * <p>
						 * NOTE: when an audio object is not used anymore, its {@link #release} method should
						 * 		 be called.
						 * 
						 * <p>
						 * This is the same interface as {@link mmir.env.media.AudioCordovaImpl}.
						 * 
						 * @class
						 * @name AudioHtml5Impl
						 * @memberOf mmir.env.media
						 * @implements mmir.env.media.IAudio
						 * @public
						 */
						var mediaImpl = {
								/**
								 * Play audio.
								 * 
								 * @inheritdoc
								 * @name play
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								play: function(){
									if (enabled){
										
										if (ready){
											
											my_media.play();
											return true;
											
										} else {
											
											var autoPlay = function(){
												
												//start auto-play only once (i.e. remove after first invocation):
												this.removeEventListener('canplay', autoPlay);
												autoPlay = null;
												
												if(enabled){
													my_media.play();
												}
											};
											my_media.addEventListener('canplay', autoPlay , false);
										}
										
									}
									return false;
								},
								/**
								 * Stop playing audio.
								 * 
								 * @inheritdoc
								 * @name stop
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								stop: function(){
									if(enabled){
										if(my_media.stop){
											//TODO really we should check first, if the audio is playing...
											my_media.stop();
										}
										else {
											my_media.pause();
											//apparently, browsers treat pause() differently: Chrome pauses, Firefox seems to stop... -> add try-catch-block in case, pause was really stop...
											try{
												my_media.currentTime=0;

												//HACK: for non-seekable audio in Chrome
												//      -> if currentTime cannot be set, we need to re-load the data
												//         (otherwise, the audio cannot be re-played!) 
												if(my_media.currentTime != 0){
													my_media.load();
												}
											}catch(e){
												return false;
											};
										}
										return true;
									}
									return false;
								},
								/**
								 * Enable audio (should only be used internally).
								 * 
								 * @inheritdoc
								 * @name enable
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								enable: function(){
									if(my_media != null){
										enabled = true;
									}
									return enabled;
								},
								/**
								 * Disable audio (should only be used internally).
								 * 
								 * @inheritdoc
								 * @name disable
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								disable: function(){
									if(enabled){
										this.stop();
										enabled = false;
									}
								},
								/**
								 * Release audio: should be called when the audio
								 * file is not used any more.
								 * 
								 * @inheritdoc
								 * @name release
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								release: function(){
									if(enabled && ! this.isPaused()){
										this.stop();
									}
									enabled= false;
									my_media=null;
								},
								/**
								 * Set the volume of this audio file
								 * 
								 * @param {Number} value
								 * 			the new value for the volume:
								 * 			a number between [0.0, 1.0]
								 * 
								 * @inheritdoc
								 * @name setVolume
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								setVolume: function(value){
									if(my_media){
										my_media.volume = value;
									}
								},
								/**
								 * Get the duration of the audio file
								 * 
								 * @returns {Number} the duration in MS (or -1 if unknown)
								 * 
								 * @inheritdoc
								 * @name getDuration
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								getDuration: function(){
									if(my_media){
										return my_media.duration;
									}
									return -1;
								},
								/**
								 * Check if audio is currently paused.
								 * 
								 * NOTE: "paused" is a different status than "stopped".
								 * 
								 * @returns {Boolean} TRUE if paused, FALSE otherwise
								 * 
								 * @inheritdoc
								 * @name isPaused
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								isPaused: function(){
									if(my_media){
										return my_media.paused;
									}
									return false;
								},
								/**
								 * Check if audio is currently enabled
								 * 
								 * @returns {Boolean} TRUE if enabled
								 * 
								 * @inheritdoc
								 * @name isEnabled
								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
								 */
								isEnabled: function(){
									return enabled;
								}
						};
						
						my_media.addEventListener('error', createErrorWrapper(mediaImpl, failureCallback), false);
						
						my_media.addEventListener('ended',
							/**
							 * @private
							 * @memberOf AudioHtml5Impl#
							 */
							function onEnded(){

								//only proceed if we have a media-object (may have already been released)
								if(enabled & mediaImpl){
									mediaImpl.stop();
								}
								if (onEnd){
									onEnd.apply(mediaImpl, arguments);
								}
							},
							false
						);
						
						//if Audio was given: "merge" with newly created Audio
						if(audioObj){
							
							extend(audioObj, mediaImpl);
							
							//transfer (possibly) changed values to newly created Audio
							if(audioObj._volume !== 1){
								audioObj.setVolume( audioObj._volume );
							}
							if(audioObj._play){
								audioObj.play();
							}
							
							//remove internal properties / impl. that are not used anymore:
							audioObj._volume  = void(0);
							audioObj._play    = void(0);
							audioObj._enabled = void(0);
							
							mediaImpl = audioObj;
						}

						return mediaImpl;

					} catch (e){
						var err = createError(0,e);
						if(failureCallback){
							failureCallback.call(mediaImpl, err, e);
						}
						else {
							console.error(err.message + ': ' + err.description, e);
						}
					}
				},//END getURLAsAudio

				/**
				 * @public
				 * @memberOf CordovaAudioOutput.prototype
				 * @see mmir.MediaManager#getAudio
				 */
				getAudio: mediaManager.getAudio,
			});
		}
};