Source: env/media/webAudio.js

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