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 newMediaPlugin = {
 29 
 30 		/**  @memberOf Html5AudioOutput# */
 31 		initialize: function(callBack, mediaManagerInstance){
 32 			
 33 			/**  @memberOf Html5AudioOutput# */
 34 			var _pluginName = 'html5AudioOutput';
 35 			
 36 			/**
 37 			 * Media error (codes):
 38 			 * 
 39 			 * the corresponding code is their <code>index</code>.
 40 			 * 
 41 			 * <p>
 42 			 * 
 43 			 * Code 0 is an internal error code for unknown/unspecific error causes.
 44 			 * <br>
 45 			 * Codes 1 - 4 correspond to the HTML5 MediaError interface
 46 			 * (which are the same as Cordova's Audio MediaError codes).
 47 			 * The description texts are taken from the HTML5 documentation.
 48 			 * 
 49 			 * @enum
 50 			 * @constant
 51 			 * @type Array<String>
 52 			 * @memberOf Html5AudioOutput#
 53 			 * 
 54 			 * @see <a href="https://html.spec.whatwg.org/multipage/embedded-content.html#mediaerror">https://html.spec.whatwg.org/multipage/embedded-content.html#mediaerror</a>
 55 			 * @see <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media">http://plugins.cordova.io/#/package/org.apache.cordova.media</a>
 56 			 */
 57 			var MediaError = [
 58 			  	{name: 'MEDIA_ERR_UNKNOWN', 		code: 0, description: 'An unknown or unspecific (internal) error occurred.'},
 59 			  	{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.'},
 60 			  	{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.'},
 61 			  	{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.'},
 62 			  	{name: 'MEDIA_ERR_NONE_SUPPORTED', 	code: 4, description: 'The media resource indicated by the src attribute or assigned media provider object was not suitable.'}
 63 			];
 64 			
 65 			/**
 66 			 * HELPER for creating error object that is returned in the failureCallbacks
 67 			 * 
 68 			 * @param {Number} code
 69 			 * 			The error code: if in [1,4], the corresponding HTML5 MediaError information will be returned
 70 			 * 			Otherwise an "error 0", i.e. internal/unknown error will be created
 71 			 * @param {Event|Error} errorEvent
 72 			 * 			the causing event- or error-object
 73 			 * 
 74 			 * @returns {Object} the error object, which has 3 properties: 
 75 			 * 			  code (Number): the error code
 76 			 * 			  message (String): the error name
 77 			 * 			  description (String): a descriptive text for the error
 78 			 * 
 79 			 * @memberOf Html5AudioOutput#
 80 			 */
 81 			function createError(code, errorEvent){
 82 				
 83 				var mErr;
 84 				if(code > 0 && code < 5){
 85 					mErr = MediaError[code];
 86 				}
 87 				else {
 88 					mErr = MediaError[0];
 89 				}
 90 				
 91 				return {
 92 						code: 			mErr.code,
 93 						message: 		mErr.name,
 94 						description: 	mErr.description + (code===0 && errorEvent? ' ' + errorEvent.toString() : '')
 95 				};
 96 			}
 97 			
 98 			/**
 99 			 * FACTORY for creating error-listeners (that trigger the failureCallback)
100 			 * 
101 			 * @param {Object} [ctx]
102 			 * 			the context for the errorCallback.
103 			 * 			IF omitted, the callback will be within the default (i.e. global) context.
104 			 * @param {Function} [errorCallback]
105 			 * 			the error callback.
106 			 * 			Is invoked with 2 arguments:
107 			 * 			errorCallback(error, event)
108 			 * 			where error has 3 properties: 
109 			 * 			  code (Number): the error code
110 			 * 			  message (String): the error name
111 			 * 			  description (String): a descriptive text for the error
112 			 * 
113 			 * 			IF omitted, the error will be printed to the console.
114 			 * 
115 			 * @return {Function} wrapper function that can be registered as event-listener (takes one argument: the event)
116 			 * 
117 			 * @memberOf Html5AudioOutput#
118 			 */
119 			function createErrorWrapper(ctx, errorCallback){
120 				
121 				return function(evt){
122 					
123 					var code;
124 					//extract MediaError from event's (audio) target:
125 					if(evt && evt.target && evt.target.error && (code = evt.target.error.code) && code > 0 && code < 5){
126 //						code = code; //NO-OP: code value was already assigned in IF clause
127 					}
128 					else {
129 						//unknown cause: create internal-error object
130 						code = 0;
131 					}
132 					
133 					var err = createError(code, evt);
134 					
135 					if(errorCallback){
136 						errorCallback.call(ctx, err, evt);
137 					}
138 					else {
139 						console.error(err.message + ' (code '+err.code + '): '+err.description, evt);
140 					}
141 				};
142 			}
143 			
144 			/**
145 			 * HELPER for creating data-URL from binary data (blob)
146 			 * 
147 			 * @param {Blob} blob
148 			 * 			The audio data as blob
149 			 * @param {Function} callback
150 			 * 			callback that will be invoked with the data-URL:
151 			 * 			<code>callback(dataUrl)</code>
152 			 * 
153 			 * @memberOf Html5AudioOutput#
154 			 */
155 			function createDataUrl(blob, callback){
156 				
157 				if(window.URL){
158 					callback( window.URL.createObjectURL(blob) );
159 				}
160 				else if(window.webkitURL){
161 					callback( window.webkitURL.createObjectURL(blob) );
162 				}
163 				else {
164 					
165 					//DEFAULT: use file-reader:
166 					var fileReader = new FileReader();
167 
168 		            // onload needed since Google Chrome doesn't support addEventListener for FileReader
169 		            fileReader.onload = function (evt) {
170 		            	// Read out "file contents" as a Data URL
171 		                var dataUrl = evt.target.result;
172 		                callback(dataUrl);
173 		            };
174 		            
175 		            //start loading the blob as Data URL:
176 		            fileReader.readAsDataURL(blob);
177 				}
178 				
179 			}
180 
181 			//invoke the passed-in initializer-callback and export the public functions:
182 			callBack({
183 				/**
184 				 * @public
185 				 * @memberOf Html5AudioOutput.prototype
186 				 * @see mmir.MediaManager#playWAV
187 				 */
188 				playWAV: function(blob, onEnd, failureCallback, successCallback){
189 					
190 					try {
191 						
192 						var self = this;
193 						createDataUrl(blob, function(dataUrl){
194 							
195 							self.playURL(dataUrl, onEnd, failureCallback, successCallback);
196 							
197 						});
198 						
199 						
200 					} catch (e){
201 						
202 						var err = createError(0,e);
203 						if(failureCallback){
204 							failureCallback.call(null, err, e);
205 						}
206 						else {
207 							console.error(err.message + ': ' + err.description, e);
208 						}
209 					}
210 				},
211 				/**
212 				 * @public
213 				 * @memberOf Html5AudioOutput.prototype
214 				 * @see mmir.MediaManager#playURL
215 				 */
216 				playURL: function(url, onEnd, failureCallback, successCallback){
217 					
218 					try {
219 						
220 						var audio = new Audio(url);
221 
222 						if(failureCallback){
223 							audio.addEventListener('error', createErrorWrapper(audio, failureCallback), false);
224 						}
225 						
226 						if(onEnd){
227 							audio.addEventListener('ended', onEnd, false);
228 						}
229 						
230 						if(successCallback){
231 							audio.addEventListener('canplay', successCallback, false);
232 						}
233 						
234 						audio.play();
235 						
236 					} catch (e){
237 						
238 						var err = createError(0,e);
239 						if(failureCallback){
240 							failureCallback.call(null, err, e);
241 						}
242 						else {
243 							console.error(err.message + ': ' + err.description, e);
244 						}
245 					}
246 					
247 				},
248 				
249 				/**
250 				 * @public
251 				 * @memberOf Html5AudioOutput.prototype
252 				 * @see mmir.MediaManager#getWAVAsAudio
253 				 */
254 				getWAVAsAudio: function(blob, callback, onEnd, failureCallback, onInit, emptyAudioObj){
255 					
256 					if(!emptyAudioObj){
257 						emptyAudioObj = mediaManagerInstance.createEmptyAudio();
258 					}
259 					
260 					try {
261 						
262 						var self = this;
263 						
264 						createDataUrl(blob, function(dataUrl){
265 							
266 							var audioObj;
267 
268 							//do not start creating the blob, if the audio was already discarded:
269 							if(emptyAudioObj.isEnabled()){
270 								audioObj = self.getURLAsAudio(dataUrl, onEnd, failureCallback, onInit, emptyAudioObj);
271 							} else {
272 								audioObj = emptyAudioObj;
273 							}
274 							
275 							if(callback){
276 								callback.call(audioObj, audioObj);
277 							}
278 							
279 						});
280 						
281 						
282 					} catch (e){
283 						
284 						var err = createError(0, e);
285 						if(failureCallback){
286 							failureCallback.call(emptyAudioObj, err, e);
287 						}
288 						else {
289 							console.error(err.message + ': ' + err.description, e);
290 						}
291 					}
292 					
293 					return emptyAudioObj;
294 				},
295 				
296 				/**
297 				 * @public
298 				 * @memberOf Html5AudioOutput.prototype
299 				 * @see mmir.MediaManager#getURLAsAudio
300 				 */
301 				getURLAsAudio: function(url, onEnd, failureCallback, successCallback, audioObj){
302 					
303 					try {
304 
305 						/**
306 						 * @private
307 						 * @memberOf AudioHtml5Impl#
308 						 */
309 						var enabled = audioObj? audioObj._enabled : true;
310 						/**
311 						 * @private
312 						 * @memberOf AudioHtml5Impl#
313 						 */
314 						var ready = false;
315 						/**
316 						 * @private
317 						 * @memberOf AudioHtml5Impl#
318 						 */
319 						var my_media = new Audio(url);
320 
321 						/**
322 						 * @private
323 						 * @memberOf AudioHtml5Impl#
324 						 */
325 						var canPlayCallback = function(){
326 							ready = true;
327 //							console.log("sound is ready!");
328 
329 							//FIX: remove this listener after first invocation 
330 							//     (this is meant as "on-init" listener, but "canplay" 
331 							//      may be triggered multiple times during the lifetime of the audio object).
332 							this.removeEventListener('canplay', canPlayCallback);
333 							canPlayCallback = null;
334 
335 							if (enabled && successCallback){
336 								successCallback.apply(mediaImpl, arguments);
337 							}
338 						};
339 						my_media.addEventListener('canplay', canPlayCallback, false);
340 						
341 						/**
342 						 * The Audio abstraction that is returned by {@link mmir.MediaManager#getURLAsAudio}.
343 						 * 
344 						 * <p>
345 						 * NOTE: when an audio object is not used anymore, its {@link #release} method should
346 						 * 		 be called.
347 						 * 
348 						 * <p>
349 						 * This is the same interface as {@link mmir.env.media.AudioCordovaImpl}.
350 						 * 
351 						 * @class
352 						 * @name AudioHtml5Impl
353 						 * @memberOf mmir.env.media
354 						 * @implements mmir.env.media.IAudio
355 						 * @public
356 						 */
357 						var mediaImpl = {
358 								/**
359 								 * Play audio.
360 								 * 
361 								 * @inheritdoc
362 								 * @name play
363 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
364 								 */
365 								play: function(){
366 									if (enabled){
367 										
368 										if (ready){
369 											my_media.play();
370 										} else {
371 											
372 											var autoPlay = function(){
373 												
374 												//start auto-play only once (i.e. remove after first invocation):
375 												this.removeEventListener('canplay', autoPlay);
376 												autoPlay = null;
377 												
378 												if(enabled){
379 													my_media.play();
380 												}
381 											};
382 											my_media.addEventListener('canplay', autoPlay , false);
383 											
384 										}
385 
386 									};
387 								},
388 								/**
389 								 * Stop playing audio.
390 								 * 
391 								 * @inheritdoc
392 								 * @name stop
393 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
394 								 */
395 								stop: function(){
396 									if(enabled){
397 										if(my_media.stop){
398 											//TODO really we should check first, if the audio is playing...
399 											my_media.stop();
400 										}
401 										else {
402 											my_media.pause();
403 											//apparently, browser treat pause() differently: Chrome pauses, Firefox seems to stop... -> add try-catch-block in case, pause was really stop...
404 											try{
405 												my_media.currentTime=0;
406 
407 												//HACK: for non-seekable audio in Chrome
408 												//      -> if currentTime cannot be set, we need to re-load the data
409 												//         (otherwise, the audio cannot be re-played!) 
410 												if(my_media.currentTime != 0){
411 													my_media.load();
412 												}
413 											}catch(e){};
414 										}
415 									}
416 								},
417 								/**
418 								 * Enable audio (should only be used internally).
419 								 * 
420 								 * @inheritdoc
421 								 * @name enable
422 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
423 								 */
424 								enable: function(){
425 									if(my_media != null){
426 										enabled = true;
427 									}
428 									return enabled;
429 								},
430 								/**
431 								 * Disable audio (should only be used internally).
432 								 * 
433 								 * @inheritdoc
434 								 * @name disable
435 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
436 								 */
437 								disable: function(){
438 									if(enabled){
439 										this.stop();
440 										enabled = false;
441 									}
442 								},
443 								/**
444 								 * Release audio: should be called when the audio
445 								 * file is not used any more.
446 								 * 
447 								 * @inheritdoc
448 								 * @name release
449 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
450 								 */
451 								release: function(){
452 									if(enabled && ! this.isPaused()){
453 										this.stop();
454 									}
455 									enabled= false;
456 									my_media=null;
457 								},
458 								/**
459 								 * Set the volume of this audio file
460 								 * 
461 								 * @param {Number} value
462 								 * 			the new value for the volume:
463 								 * 			a number between [0.0, 1.0]
464 								 * 
465 								 * @inheritdoc
466 								 * @name setVolume
467 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
468 								 */
469 								setVolume: function(value){
470 									if(my_media){
471 										my_media.volume = value;
472 									}
473 								},
474 								/**
475 								 * Get the duration of the audio file
476 								 * 
477 								 * @returns {Number} the duration in MS (or -1 if unknown)
478 								 * 
479 								 * @inheritdoc
480 								 * @name getDuration
481 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
482 								 */
483 								getDuration: function(){
484 									if(my_media){
485 										return my_media.duration;
486 									}
487 									return -1;
488 								},
489 								/**
490 								 * Check if audio is currently paused.
491 								 * 
492 								 * NOTE: "paused" is a different status than "stopped".
493 								 * 
494 								 * @returns {Boolean} TRUE if paused, FALSE otherwise
495 								 * 
496 								 * @inheritdoc
497 								 * @name isPaused
498 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
499 								 */
500 								isPaused: function(){
501 									if(my_media){
502 										return my_media.paused;
503 									}
504 									return false;
505 								},
506 								/**
507 								 * Check if audio is currently enabled
508 								 * 
509 								 * @returns {Boolean} TRUE if enabled
510 								 * 
511 								 * @inheritdoc
512 								 * @name isEnabled
513 								 * @memberOf mmir.env.media.AudioHtml5Impl.prototype
514 								 */
515 								isEnabled: function(){
516 									return enabled;
517 								}
518 						};
519 						
520 						my_media.addEventListener('error', createErrorWrapper(mediaImpl, failureCallback), false);
521 						
522 						my_media.addEventListener('ended',
523 							/**
524 							 * @private
525 							 * @memberOf AudioHtml5Impl#
526 							 */
527 							function onEnded(){
528 
529 								//only proceed if we have a media-object (may have already been released)
530 								if(enabled & mediaImpl){
531 									mediaImpl.stop();
532 								}
533 								if (onEnd){
534 									onEnd.apply(mediaImpl, arguments);
535 								}
536 							},
537 							false
538 						);
539 						
540 						//if Audio was given: "merge" with newly created Audio
541 						if(audioObj){
542 							
543 							jQuery.extend(audioObj, mediaImpl);
544 							
545 							//transfer (possibly) changed values to newly created Audio
546 							if(audioObj._volume !== 1){
547 								audioObj.setVolume( audioObj._volume );
548 							}
549 							if(audioObj._play){
550 								audioObj.play();
551 							}
552 							
553 							//remove internal properties / impl. that are not used anymore:
554 							audioObj._volume  = void(0);
555 							audioObj._play    = void(0);
556 							audioObj._enabled = void(0);
557 							
558 							mediaImpl = audioObj;
559 						}
560 
561 						return mediaImpl;
562 
563 					} catch (e){
564 						var err = createError(0,e);
565 						if(failureCallback){
566 							failureCallback.call(mediaImpl, err, e);
567 						}
568 						else {
569 							console.error(err.message + ': ' + err.description, e);
570 						}
571 					}
572 				}
573 			});
574 		}
575 };
576