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