Source: env/media/webAudio.js


define(['mmirf/mediaManager', 'mmirf/util/extend'], function(mediaManager, extend){

var globalCtx = typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this;

/**
 * Audio handling for web / HTML5 environment
 *
 * @class
 * @name Html5AudioOutput
 * @memberOf mmir.env.media
 * @hideconstructor
 *
 * @requires window.URL or window.webkitURL for #playWAV
 */
return {

	/**  @memberOf mmir.env.media.Html5AudioOutput# */
	initialize: function(callBack){

		/**
		 * @default "webAudio"
		 * @readonly
		 * @protected
		 * @memberOf mmir.env.media.Html5AudioOutput#
		 */
		var _pluginName = 'webAudio';

		/**
		 * 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 mmir.env.media.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
		 *
		 * @private
		 * @memberOf mmir.env.media.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)
		 *
		 * @private
		 * @memberOf mmir.env.media.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 {
					mediaManager._log.error('['+_pluginName+'] '+ 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>
		 *
		 * @private
		 * @memberOf mmir.env.media.Html5AudioOutput#
		 */
		function createDataUrl(blob, callback){

			if(globalCtx.URL || globalCtx.webkitURL){
				callback( (globalCtx.URL || globalCtx.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);
			}

		}

		/**
		 * HELPER for releasing data-URL
		 *
		 * @param {String} dataUrl
		 * 			The data URL for the audio blob
		 *
		 * @private
		 * @memberOf mmir.env.media.Html5AudioOutput#
		 */
		function releaseDataUrl(dataUrl){

			if(globalCtx.URL || globalCtx.webkitURL){
				(globalCtx.URL || globalCtx.webkitURL).revokeObjectURL(dataUrl);
			}
			else {
				mediaManager._log.d('cannot release media URL: no URL.revokeObjectURL() available!')
			}

		}

		/**
		 * HELPER for handling new play() promise mechanism:
		 * play may get rejected due to no user-interaction -> trigger 'errorplay' event on MediaManager
		 *
		 * @param {Promise|void} p
		 * 			The promise returned by calling WebAudio.play() (or FALSY, if no promise was returned)
		 * @param {IAudio} audio
		 * 			The audio object for which play was invoked
		 * @param {Function} errorHandler
		 * 			The error handler/callback for the audio
		 *
		 * @returns {mmir.env.media.IAudio} the audio
		 *
		 * @private
		 * @memberOf mmir.env.media.Html5AudioOutput#
		 */
		function handlePlayPromise(p, audio, errorHandler) {
			if(p && p.catch){
				p.catch(function(err){
					mediaManager._emitEvent('errorplay', audio, err);
					errorHandler.call(audio, {
						code: 			err.code,
						message: 		err.name,
						description: 	err.message
					});
				});
			}
		}

		//invoke the passed-in initializer-callback and export the public functions:
		callBack({
			/**
			 * @public
			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
			 * @see mmir.MediaManager#playWAV
			 * @copydoc mmir.MediaManager#playWAV
			 */
			playWAV: function(blob, onEnd, failureCallback, successCallback){

				try {

					var self = this;
					createDataUrl(blob, function(dataUrl){

						self.playURL(dataUrl,
								function onend(){
									releaseDataUrl(dataUrl);
									onEnd && onEnd.apply(this, arguments);
								},
								failureCallback, successCallback);

					});


				} catch (e){

					var err = createError(0,e);
					if(failureCallback){
						failureCallback.call(null, err, e);
					}
					else {
						mediaManager._log.error('['+_pluginName+'] '+ err.message + ': ' + err.description, e);
					}
				}
			},
			/**
			 * @public
			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
			 * @see mmir.MediaManager#playURL
			 * @copydoc 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);
					}

					var p = audio.play();
					handlePlayPromise(p, audio, failureCallback);

				} catch (e){

					var err = createError(0,e);
					if(failureCallback){
						failureCallback.call(null, err, e);
					}
					else {
						mediaManager._log.error('['+_pluginName+'] '+ err.message + ': ' + err.description, e);
					}
				}

			},
			/**
 			 * @public
 			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
 			 * @see mmir.MediaManager#play
 			 * @copydoc mmir.MediaManager#play
 			 * @function play
 			 */
			play: mediaManager.play,

			/**
			 * @public
			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
			 * @see mmir.MediaManager#getWAVAsAudio
			 * @copydoc 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);
							audioObj._wavrelease = audioObj.release;
							audioObj.release = function(){
								if(this.isEnabled()){
									releaseDataUrl(dataUrl);
								}
								return this._wavrelease();
							};
						} else {
							audioObj = emptyAudioObj;
						}

						if(callback){
							callback.call(audioObj, audioObj);
						}

					});


				} catch (e){

					var err = createError(0, e);
					if(failureCallback){
						failureCallback.call(emptyAudioObj, err, e);
					}
					else {
						mediaManager._log.error('['+_pluginName+'] '+ err.message + ': ' + err.description, e);
					}
				}

				return emptyAudioObj;
			},

			/**
			 * @public
			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
			 * @see mmir.MediaManager#getURLAsAudio
			 * @copydoc 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
					 * @hideconstructor
					 */
					var mediaImpl = {
							/**
							 * Play audio.
							 *
							 * @inheritdoc
							 * @name play
							 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
							 */
							play: function(){
								if (enabled){

									if (ready){

										var p = my_media.play();
										handlePlayPromise(p, mediaImpl, failureCallback);

										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){
												var p = my_media.play();
												handlePlayPromise(p, mediaImpl, failureCallback);
											}
										};
										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 {
						mediaManager._log.error('['+_pluginName+'] '+ err.message + ': ' + err.description, e);
					}
				}
			},//END getURLAsAudio

			/**
			 * @public
			 * @memberOf mmir.env.media.Html5AudioOutput.prototype
			 * @see mmir.MediaManager#getAudio
			 * @copydoc mmir.MediaManager#getAudio
			 * @function
			 */
			getAudio: mediaManager.getAudio,
		});
	}
};

});//END define