1// A cross-browser javascript shim for html5 audio 2(function (root, factory) { 3 if (typeof define === 'function' && define.cmd) { 4 define(factory); 5 } else { 6 root.audiojs = factory(); 7 } 8}(this, function (require, exports, module) { 9 10 var container = {}, audiojsInstance = 'audiojsInstance', audiojs = 'audiojs'; 11 // Use the path to the audio.js file to create relative paths to the swf and player graphics 12 // Remember that some systems (e.g. ruby on rails) append strings like '?1301478336' to asset paths 13 var path = (function () { 14 var re = new RegExp('audio(\.min)?\.js.*'), 15 scripts = document.getElementsByTagName('script'); 16 for (var i = 0, ii = scripts.length; i < ii; i++) { 17 var path = scripts[i].getAttribute('src'); 18 if (re.test(path)) return path.replace(re, ''); 19 } 20 })(); 21 22 // ##The audiojs interface 23 // This is the global object which provides an interface for creating new `audiojs` instances. 24 // It also stores all of the construction helper methods and variables. 25 container[audiojs] = { 26 instanceCount: 0, 27 instances: {}, 28 // The markup for the swf. It is injected into the page if there is not support for the `<audio>` element. The `$n`s are placeholders. 29 // `$1` The name of the flash movie 30 // `$2` The path to the swf 31 // `$3` Cache invalidation 32 flashSource: '\ 33 <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="$1" width="1" height="1" name="$1" style="position: absolute; left: -1px;"> \ 34 <param name="movie" value="$2?playerInstance=' + audiojs + '.instances[\'$1\']&datetime=$3"> \ 35 <param name="allowscriptaccess" value="always"> \ 36 <embed name="$1" src="$2?playerInstance=' + audiojs + '.instances[\'$1\']&datetime=$3" width="1" height="1" allowscriptaccess="always"> \ 37 </object>', 38 39 // ### The main settings object 40 // Where all the default settings are stored. Each of these variables and methods can be overwritten by the user-provided `options` object. 41 settings: { 42 autoplay: false, 43 loop: false, 44 preload: true, 45 imageLocation: path + 'player-graphics.gif', 46 swfLocation: path + 'audiojs.swf', 47 useFlash: (function () { 48 var a = document.createElement('audio'); 49 return !(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, '')); 50 })(), 51 hasFlash: (function () { 52 if (navigator.plugins && navigator.plugins.length && navigator.plugins['Shockwave Flash']) { 53 return true; 54 } else if (navigator.mimeTypes && navigator.mimeTypes.length) { 55 var mimeType = navigator.mimeTypes['application/x-shockwave-flash']; 56 return mimeType && mimeType.enabledPlugin; 57 } else { 58 try { 59 var ax = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); 60 return true; 61 } catch (e) { 62 } 63 } 64 return false; 65 })(), 66 // The default markup and classes for creating the player: 67 createPlayer: { 68 markup: '\ 69 <div class="play-pause"> \ 70 <p class="play"></p> \ 71 <p class="pause"></p> \ 72 <p class="loading"></p> \ 73 <p class="error"></p> \ 74 </div> \ 75 <div class="scrubber"> \ 76 <div class="progress"></div> \ 77 <div class="loaded"></div> \ 78 </div> \ 79 <div class="time"> \ 80 <em class="played">00:00</em>/<strong class="duration">00:00</strong> \ 81 </div> \ 82 <div class="error-message"></div>', 83 playPauseClass: 'play-pause', 84 scrubberClass: 'scrubber', 85 progressClass: 'progress', 86 loaderClass: 'loaded', 87 timeClass: 'time', 88 durationClass: 'duration', 89 playedClass: 'played', 90 errorMessageClass: 'error-message', 91 playingClass: 'playing', 92 loadingClass: 'loading', 93 errorClass: 'error' 94 }, 95 // The css used by the default player. This is is dynamically injected into a `<style>` tag in the top of the head. 96 css: '\ 97 .audiojs audio { position: absolute; left: -1px; } \ 98 .audiojs { width: 460px; height: 36px; background: #404040; overflow: hidden; font-family: monospace; font-size: 12px; \ 99 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #444), color-stop(0.5, #555), color-stop(0.51, #444), color-stop(1, #444)); \ 100 background-image: -moz-linear-gradient(center top, #444 0%, #555 50%, #444 51%, #444 100%); \ 101 -webkit-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); -moz-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); \
102 -o-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); } \ 103 .audiojs .play-pause { width: 25px; height: 40px; padding: 4px 6px; margin: 0px; float: left; overflow: hidden; border-right: 1px solid #000; } \ 104 .audiojs p { display: none; width: 25px; height: 40px; margin: 0px; cursor: pointer; } \ 105 .audiojs .play { display: block; } \ 106 .audiojs .scrubber { position: relative; float: left; width: 280px; background: #5a5a5a; height: 14px; margin: 10px; border-top: 1px solid #3f3f3f; border-left: 0px; border-bottom: 0px; overflow: hidden; } \ 107 .audiojs .progress { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #ccc; z-index: 1; \ 108 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ccc), color-stop(0.5, #ddd), color-stop(0.51, #ccc), color-stop(1, #ccc)); \ 109 background-image: -moz-linear-gradient(center top, #ccc 0%, #ddd 50%, #ccc 51%, #ccc 100%); } \ 110 .audiojs .loaded { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #000; \ 111 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #222), color-stop(0.5, #333), color-stop(0.51, #222), color-stop(1, #222)); \ 112 background-image: -moz-linear-gradient(center top, #222 0%, #333 50%, #222 51%, #222 100%); } \ 113 .audiojs .time { float: left; height: 36px; line-height: 36px; margin: 0px 0px 0px 6px; padding: 0px 6px 0px 12px; border-left: 1px solid #000; color: #ddd; text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.5); } \ 114 .audiojs .time em { padding: 0px 2px 0px 0px; color: #f9f9f9; font-style: normal; } \ 115 .audiojs .time strong { padding: 0px 0px 0px 2px; font-weight: normal; } \ 116 .audiojs .error-message { float: left; display: none; margin: 0px 10px; height: 36px; width: 400px; overflow: hidden; line-height: 36px; white-space: nowrap; color: #fff; \ 117 text-overflow: ellipsis; -o-text-overflow: ellipsis; -icab-text-overflow: ellipsis; -khtml-text-overflow: ellipsis; -moz-text-overflow: ellipsis; -webkit-text-overflow: ellipsis; } \ 118 .audiojs .error-message a { color: #eee; text-decoration: none; padding-bottom: 1px; border-bottom: 1px solid #999; white-space: wrap; } \ 119 \ 120 .audiojs .play { background: url("$1") -2px -1px no-repeat; } \ 121 .audiojs .loading { background: url("$1") -2px -31px no-repeat; } \ 122 .audiojs .error { background: url("$1") -2px -61px no-repeat; } \ 123 .audiojs .pause { background: url("$1") -2px -91px no-repeat; } \ 124 \ 125 .playing .play, .playing .loading, .playing .error { display: none; } \ 126 .playing .pause { display: block; } \ 127 \ 128 .loading .play, .loading .pause, .loading .error { display: none; } \ 129 .loading .loading { display: block; } \ 130 \ 131 .error .time, .error .play, .error .pause, .error .scrubber, .error .loading { display: none; } \ 132 .error .error { display: block; } \ 133 .error .play-pause p { cursor: auto; } \ 134 .error .error-message { display: block; }', 135 // The default event callbacks: 136 trackEnded: function (e) { 137 }, 138 flashError: function () { 139 var player = this.settings.createPlayer, 140 errorMessage = getByClass(player.errorMessageClass, this.wrapper), 141 html = 'Missing <a href="http://get.adobe.com/flashplayer/">flash player</a> plugin.'; 142 if (this.mp3) html += ' <a href="' + this.mp3 + '">Download audio file</a>.'; 143 container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass); 144 container[audiojs].helpers.addClass(this.wrapper, player.errorClass); 145 errorMessage.innerHTML = html; 146 }, 147 loadError: function (e) { 148 var player = this.settings.createPlayer, 149 errorMessage = getByClass(player.errorMessageClass, this.wrapper); 150 container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass); 151 container[audiojs].helpers.addClass(this.wrapper, player.errorClass); 152 errorMessage.innerHTML = 'Error loading: "' + this.mp3 + '"'; 153 }, 154 init: function () { 155 var player = this.settings.createPlayer; 156 container[audiojs].helpers.addClass(this.wrapper, player.loadingClass); 157 }, 158 loadStarted: function () { 159 var player = this.settings.createPlayer, 160 duration = getByClass(player.durationClass, this.wrapper), 161 m = Math.floor(this.duration / 60), 162 s = Math.floor(this.duration % 60); 163 container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass); 164 duration.innerHTML = ((m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s); 165 }, 166 loadProgress: function (percent) { 167 var player = this.settings.createPlayer, 168 loaded = getByClass(player.loaderClass, this.wrapper); 169 loaded.style.width = (100 * percent) + '%'; 170 }, 171 playPause: function () { 172 if (this.playing) this.settings.play(); 173 else this.settings.pause(); 174 }, 175 play: function () { 176 var player = this.settings.createPlayer; 177 container[audiojs].helpers.addClass(this.wrapper, player.playingClass); 178 }, 179 pause: function () { 180 var player = this.settings.createPlayer; 181 container[audiojs].helpers.removeClass(this.wrapper, player.playingClass); 182 }, 183 updatePlayhead: function (percent) { 184 var player = this.settings.createPlayer, 185 progress = getByClass(player.progressClass, this.wrapper); 186 progress.style.width = (100 * percent) + '%'; 187 188 var played = getByClass(player.playedClass, this.wrapper), 189 p = this.duration * percent, 190 m = Math.floor(p / 60), 191 s = Math.floor(p % 60); 192 played.innerHTML = ((m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s); 193 } 194 }, 195 196 // ### Contructor functions 197 198 // `create()` 199 // Used to create a single `audiojs` instance. 200 // If an array is passed then it calls back to `createAll()`. 201 // Otherwise, it creates a single instance and returns it. 202 create: function (element, options) { 203 var options = options || {}; 204 if (element.length) { 205 return this.createAll(options, element); 206 } else { 207 return this.newInstance(element, options); 208 } 209 }, 210 211 // `createAll()` 212 // Creates multiple `audiojs` instances. 213 // If `elements` is `null`, then automatically find any `<audio>` tags on the page and create `audiojs` instances for them. 214 createAll: function (options, elements) { 215 var audioElements = elements || document.getElementsByTagName('audio'), 216 instances = []; 217 options = options || {}; 218 for (var i = 0, ii = audioElements.length; i < ii; i++) { 219 instances.push(this.newInstance(audioElements[i], options)); 220 } 221 return instances; 222 }, 223 224 // ### Creating and returning a new instance 225 // This goes through all the steps required to build out a usable `audiojs` instance. 226 newInstance: function (element, options) { 227 var element = element, 228 s = this.helpers.clone(this.settings), 229 id = 'audiojs' + this.instanceCount, 230 wrapperId = 'audiojs_wrapper' + this.instanceCount, 231 instanceCount = this.instanceCount++; 232 233 // Check for `autoplay`, `loop` and `preload` attributes and write them into the settings. 234 if (element.getAttribute('autoplay') != null) s.autoplay = true; 235 if (element.getAttribute('loop') != null) s.loop = true; 236 if (element.getAttribute('preload') == 'none') s.preload = false;
237 // Merge the default settings with the user-defined `options`. 238 if (options) this.helpers.merge(s, options); 239 240 // Inject the player html if required. 241 if (s.createPlayer.markup) element = this.createPlayer(element, s.createPlayer, wrapperId); 242 else element.parentNode.setAttribute('id', wrapperId); 243 244 // Return a new `audiojs` instance. 245 var audio = new container[audiojsInstance](element, s); 246 247 // If css has been passed in, dynamically inject it into the `<head>`. 248 if (s.css) this.helpers.injectCss(audio, s.css); 249 250 // If `<audio>` or mp3 playback isn't supported, insert the swf & attach the required events for it. 251 if (s.useFlash && s.hasFlash) { 252 this.injectFlash(audio, id); 253 this.attachFlashEvents(audio.wrapper, audio); 254 } else if (s.useFlash && !s.hasFlash) { 255 this.settings.flashError.apply(audio); 256 } 257 258 // Attach event callbacks to the new audiojs instance. 259 if (!s.useFlash || (s.useFlash && s.hasFlash)) this.attachEvents(audio.wrapper, audio); 260 261 // Store the newly-created `audiojs` instance. 262 this.instances[id] = audio; 263 return audio; 264 }, 265 266 // ### Helper methods for constructing a working player 267 // Inject a wrapping div and the markup for the html player. 268 createPlayer: function (element, player, id) { 269 var wrapper = document.createElement('div'), 270 newElement = element.cloneNode(true); 271 wrapper.setAttribute('class', 'audiojs'); 272 wrapper.setAttribute('className', 'audiojs'); 273 wrapper.setAttribute('id', id); 274 275 // Fix IE's broken implementation of `innerHTML` & `cloneNode` for HTML5 elements. 276 if (newElement.outerHTML && !document.createElement('audio').canPlayType) { 277 newElement = this.helpers.cloneHtml5Node(element); 278 wrapper.innerHTML = player.markup; 279 wrapper.appendChild(newElement); 280 element.outerHTML = wrapper.outerHTML; 281 wrapper = document.getElementById(id); 282 } else { 283 wrapper.appendChild(newElement); 284 wrapper.innerHTML = wrapper.innerHTML + player.markup; 285 element.parentNode.replaceChild(wrapper, element); 286 } 287 return wrapper.getElementsByTagName('audio')[0]; 288 }, 289 290 // Attaches useful event callbacks to an `audiojs` instance. 291 attachEvents: function (wrapper, audio) { 292 if (!audio.settings.createPlayer) return; 293 var player = audio.settings.createPlayer, 294 playPause = getByClass(player.playPauseClass, wrapper), 295 scrubber = getByClass(player.scrubberClass, wrapper), 296 leftPos = function (elem) { 297 var curleft = 0; 298 if (elem.offsetParent) { 299 do { 300 curleft += elem.offsetLeft; 301 } while (elem = elem.offsetParent); 302 } 303 return curleft; 304 }; 305 306 307 container[audiojs].events.addListener(playPause, 'click', function (e) { 308 audio.playPause.apply(audio); 309 }); 310 311 container[audiojs].events.addListener(scrubber, 'click', function (e) { 312 var relativeLeft = e.clientX - leftPos(this); 313 audio.skipTo(relativeLeft / scrubber.offsetWidth); 314 }); 315 316 // _If flash is being used, then the following handlers don't need to be registered._ 317 if (audio.settings.useFlash) return; 318 319 // Start tracking the load progress of the track. 320 container[audiojs].events.trackLoadProgress(audio); 321 322 audio.element.load(); 323 324 if ((/(ipod|iphone|ipad)/i).test(navigator.userAgent)) { 325 setTimeout(() => { 326 audio.element.muted = true; 327 audio.element.play(); 328 setTimeout(() => { 329 audio.element.muted = false;
330 audio.element.pause(); 331 }); 332 }, 2000); 333 } 334 335 336 container[audiojs].events.addListener(audio.element, 'timeupdate', function (e) { 337 audio.updatePlayhead.apply(audio); 338 }); 339 340 container[audiojs].events.addListener(audio.element, 'ended', function (e) { 341 audio.trackEnded.apply(audio); 342 }); 343 344 container[audiojs].events.addListener(audio.source, 'error', function (e) { 345 // on error, cancel any load timers that are running. 346 clearInterval(audio.readyTimer); 347 clearInterval(audio.loadTimer); 348 audio.settings.loadError.apply(audio); 349 }); 350 351 }, 352 353 // Flash requires a slightly different API to the `<audio>` element, so this method is used to overwrite the standard event handlers. 354 attachFlashEvents: function (element, audio) { 355 audio['swfReady'] = false; 356 audio['load'] = function (mp3) { 357 // If the swf isn't ready yet then just set `audio.mp3`. `init()` will load it in once the swf is ready. 358 audio.mp3 = mp3; 359 if (audio.swfReady) audio.element.load(mp3); 360 }; 361 audio['loadProgress'] = function (percent, duration) { 362 audio.loadedPercent = percent; 363 audio.duration = duration; 364 audio.settings.loadStarted.apply(audio); 365 audio.settings.loadProgress.apply(audio, [percent]); 366 }; 367 audio['skipTo'] = function (percent) { 368 if (percent > audio.loadedPercent) return; 369 audio.updatePlayhead.call(audio, [percent]); 370 audio.element.skipTo(percent); 371 }; 372 audio['updatePlayhead'] = function (percent) { 373 audio.settings.updatePlayhead.apply(audio, [percent]); 374 }; 375 audio['play'] = function () { 376 // If the audio hasn't started preloading, then start it now. 377 // Then set `preload` to `true`, so that any tracks loaded in subsequently are loaded straight away. 378 if (!audio.settings.preload) { 379 audio.settings.preload = true; 380 audio.element.init(audio.mp3); 381 } 382 audio.playing = true; 383 // IE doesn't allow a method named `play()` to be exposed through `ExternalInterface`, so lets go with `pplay()`. 384 // <http://dev.nuclearrooster.com/2008/07/27/externalinterfaceaddcallback-can-cause-ie-js-errors-with-certain-keyworkds/> 385 audio.element.pplay(); 386 audio.settings.play.apply(audio); 387 }; 388 audio['pause'] = function () { 389 audio.playing = false; 390 // Use `ppause()` for consistency with `pplay()`, even though it isn't really required. 391 audio.element.ppause(); 392 audio.settings.pause.apply(audio); 393 }; 394 audio['setVolume'] = function (v) { 395 audio.element.setVolume(v); 396 }; 397 audio['loadStarted'] = function () { 398 // Load the mp3 specified by the audio element into the swf. 399 audio.swfReady = true; 400 if (audio.settings.preload) audio.element.init(audio.mp3); 401 if (audio.settings.autoplay) audio.play.apply(audio); 402 }; 403 }, 404 405 // ### Injecting an swf from a string 406 // Build up the swf score by replacing the `$keys` and then inject the markup into the page. 407 injectFlash: function (audio, id) { 408 var flashSource = this.flashSource.replace(/\$1/g, id); 409 flashSource = flashSource.replace(/\$2/g, audio.settings.swfLocation); 410 // `(+new Date)` ensures the swf is not pulled out of cache. The fixes an issue with Firefox running multiple players on the same page. 411 flashSource = flashSource.replace(/\$3/g, (+new Date + Math.random())); 412 // Inject the player markup using a more verbose `innerHTML` insertion technique that works with IE. 413 var html = audio.wrapper.innerHTML, 414 div = document.createElement('div'); 415 div.innerHTML = flashSource + html; 416 audio.wrapper.innerHTML = div.innerHTML; 417 audio.element = this.helpers.getSwf(id); 418 }, 419 420 // ## Helper functions 421 helpers: { 422 // **Merge two objects, with `obj2` overwriting `obj1`** 423 // The merge is shallow, but that's all that is required for our purposes. 424 merge: function (obj1, obj2) { 425 for (attr in obj2) { 426 if (obj1.hasOwnProperty(attr) || obj2.hasOwnProperty(attr)) { 427 obj1[attr] = obj2[attr]; 428 } 429 } 430 }, 431 // **Clone a javascript object (recursively)** 432 clone: function (obj) { 433 if (obj == null || typeof (obj) !== 'object') return obj; 434 var temp = new obj.constructor(); 435 for (var key in obj) temp[key] = arguments.callee(obj[key]); 436 return temp; 437 }, 438 // **Adding/removing classnames from elements** 439 addClass: function (element, className) { 440 var re = new RegExp('(\\s|^)' + className + '(\\s|$)'); 441 if (re.test(element.className)) return; 442 element.className += ' ' + className; 443 }, 444 removeClass: function (element, className) { 445 var re = new RegExp('(\\s|^)' + className + '(\\s|$)'); 446 element.className = element.className.replace(re, ' '); 447 }, 448 // **Dynamic CSS injection** 449 // Takes a string of css, inserts it into a `<style>`, then injects it in at the very top of the `<head>`. This ensures any user-defined styles will take precedence. 450 injectCss: function (audio, string) { 451 452 // If an `audiojs` `<style>` tag already exists, then append to it rather than creating a whole new `<style>`. 453 var prepend = '', 454 styles = document.getElementsByTagName('style'), 455 css = string.replace(/\$1/g, audio.settings.imageLocation); 456 457 for (var i = 0, ii = styles.length; i < ii; i++) { 458 var title = styles[i].getAttribute('title'); 459 if (title && ~title.indexOf('audiojs')) { 460 style = styles[i]; 461 if (style.innerHTML === css) return; 462 prepend = style.innerHTML; 463 break; 464 } 465 } 466 ; 467 468 var head = document.getElementsByTagName('head')[0], 469 firstchild = head.firstChild, 470 style = document.createElement('style'); 471 472 if (!head) return; 473 474 style.setAttribute('type', 'text/css'); 475 style.setAttribute('title', 'audiojs'); 476 477 if (style.styleSheet) style.styleSheet.cssText = prepend + css; 478 else style.appendChild(document.createTextNode(prepend + css)); 479 480 if (firstchild) head.insertBefore(style, firstchild); 481 else head.appendChild(styleElement); 482 }, 483 // **Handle all the IE6+7 requirements for cloning `<audio>` nodes** 484 // Create a html5-safe document fragment by injecting an `<audio>` element into the document fragment. 485 cloneHtml5Node: function (audioTag) { 486 var fragment = document.createDocumentFragment(), 487 doc = fragment.createElement ? fragment : document; 488 doc.createElement('audio'); 489 var div = doc.createElement('div'); 490 fragment.appendChild(div); 491 div.innerHTML = audioTag.outerHTML; 492 return div.firstChild; 493 },
494 // **Cross-browser `<object>` / `<embed>` element selection** 495 getSwf: function (name) { 496 var swf = document[name] || window[name]; 497 return swf.length > 1 ? swf[swf.length - 1] : swf; 498 } 499 }, 500 // ## Event-handling 501 events: { 502 memoryLeaking: false, 503 listeners: [], 504 // **A simple cross-browser event handler abstraction** 505 addListener: function (element, eventName, func) { 506 // For modern browsers use the standard DOM-compliant `addEventListener`. 507 if (element.addEventListener) { 508 element.addEventListener(eventName, func, false); 509 // For older versions of Internet Explorer, use `attachEvent`. 510 // Also provide a fix for scoping `this` to the calling element and register each listener so the containing elements can be purged on page unload. 511 } else if (element.attachEvent) { 512 this.listeners.push(element); 513 if (!this.memoryLeaking) { 514 window.attachEvent('onunload', function () { 515 if (this.listeners) { 516 for (var i = 0, ii = this.listeners.length; i < ii; i++) { 517 container[audiojs].events.purge(this.listeners[i]); 518 } 519 } 520 }); 521 this.memoryLeaking = true; 522 } 523 element.attachEvent('on' + eventName, function () { 524 func.call(element, window.event); 525 }); 526 } 527 }, 528 529 trackLoadProgress: function (audio) { 530 // If `preload` has been set to `none`, then we don't want to start loading the track yet. 531 if (!audio.settings.preload) return; 532 533 var readyTimer, 534 loadTimer, 535 audio = audio, 536 ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent); 537 538 // Use timers here rather than the official `progress` event, as Chrome has issues calling `progress` when loading mp3 files from cache. 539 readyTimer = setInterval(function () { 540 if (audio.element.readyState > -1) { 541 // iOS doesn't start preloading the mp3 until the user interacts manually, so this stops the loader being displayed prematurely. 542 if (!ios) audio.init.apply(audio); 543 } 544 if (audio.element.readyState > 1) { 545 if (audio.settings.autoplay) audio.play.apply(audio); 546 clearInterval(readyTimer); 547 // Once we have data, start tracking the load progress. 548 loadTimer = setInterval(function () { 549 audio.loadProgress.apply(audio); 550 if (audio.loadedPercent >= 1) clearInterval(loadTimer); 551 }, 200); 552 } 553 }, 200); 554 audio.readyTimer = readyTimer; 555 audio.loadTimer = loadTimer; 556 }, 557 558 // **Douglas Crockford's IE6 memory leak fix** 559 // <http://javascript.crockford.com/memory/leak.html> 560 // This is used to release the memory leak created by the circular references created when fixing `this` scoping for IE. It is called on page unload. 561 purge: function (d) { 562 var a = d.attributes, i; 563 if (a) { 564 for (i = 0; i < a.length; i += 1) { 565 if (typeof d[a[i].name] === 'function') d[a[i].name] = null; 566 } 567 } 568 a = d.childNodes; 569 if (a) { 570 for (i = 0; i < a.length; i += 1) purge(d.childNodes[i]); 571 } 572 }, 573 574 // **DOMready function** 575 // As seen here: <https://github.com/dperini/ContentLoaded/>. 576 ready: (function () { 577 return function (fn) { 578 var win = window, done = false, top = true, 579 doc = win.document, root = doc.documentElement, 580 add = doc.addEventListener ? 'addEventListener' : 'attachEvent', 581 rem = doc.addEventListener ? 'removeEventListener' : 'detachEvent', 582 pre = doc.addEventListener ? '' : 'on', 583 init = function (e) { 584 if (e.type == 'readystatechange' && doc.readyState != 'complete') return; 585 (e.type == 'load' ? win : doc)[rem](pre + e.type, init, false); 586 if (!done && (done = true)) fn.call(win, e.type || e); 587 }, 588 poll = function () { 589 try { 590 root.doScroll('left'); 591 } catch (e) { 592 setTimeout(poll, 50); 593 return; 594 } 595 init('poll'); 596 }; 597 if (doc.readyState == 'complete') fn.call(win, 'lazy'); 598 else { 599 if (doc.createEventObject && root.doScroll) { 600 try { 601 top = !win.frameElement; 602 } catch (e) { 603 } 604 if (top) poll(); 605 } 606 doc[add](pre + 'DOMContentLoaded', init, false); 607 doc[add](pre + 'readystatechange', init, false); 608 win[add](pre + 'load', init, false); 609 } 610 }; 611 })() 612 613 } 614 }; 615 616 // ## The audiojs class 617 // We create one of these per `<audio>` and then push them into `audiojs['instances']`. 618 container[audiojsInstance] = function (element, settings) { 619 // Each audio instance returns an object which contains an API back into the `<audio>` element. 620 this.element = element; 621 this.wrapper = element.parentNode; 622 this.source = element.getElementsByTagName('source')[0] || element; 623 // First check the `<audio>` element directly for a src and if one is not found, look for a `<score>` element. 624 this.mp3 = (function (element) { 625 var source = element.getElementsByTagName('source')[0]; 626 return element.getAttribute('src') || (source ? source.getAttribute('src') : null); 627 })(element); 628 this.settings = settings; 629 this.loadStartedCalled = false;
630 this.loadedPercent = 0; 631 this.duration = 1; 632 this.playing = false; 633 }; 634 635 container[audiojsInstance].prototype = { 636 // API access events: 637 // Each of these do what they need do and then call the matching methods defined in the settings object. 638 updatePlayhead: function () { 639 var percent = this.element.currentTime / this.duration; 640 this.settings.updatePlayhead.apply(this, [percent]); 641 }, 642 skipTo: function (percent) { 643 if (percent > this.loadedPercent) return; 644 this.element.currentTime = this.duration * percent; 645 this.updatePlayhead(); 646 }, 647 load: function (mp3) { 648 this.loadStartedCalled = false; 649 this.source.setAttribute('src', mp3); 650 // The now outdated `load()` method is required for Safari 4 651 this.element.load(); 652 this.mp3 = mp3; 653 container[audiojs].events.trackLoadProgress(this); 654 }, 655 loadError: function () { 656 this.settings.loadError.apply(this); 657 }, 658 init: function () { 659 this.settings.init.apply(this); 660 }, 661 loadStarted: function () { 662 // Wait until `element.duration` exists before setting up the audio player. 663 if (!this.element.duration) return false; 664 665 this.duration = this.element.duration; 666 this.updatePlayhead(); 667 this.settings.loadStarted.apply(this); 668 }, 669 loadProgress: function () { 670 if (this.element.buffered != null && this.element.buffered.length) { 671 // Ensure `loadStarted()` is only called once. 672 if (!this.loadStartedCalled) { 673 this.loadStartedCalled = this.loadStarted(); 674 } 675 var durationLoaded = this.element.buffered.end(this.element.buffered.length - 1); 676 this.loadedPercent = durationLoaded / this.duration; 677 678 this.settings.loadProgress.apply(this, [this.loadedPercent]); 679 } 680 }, 681 playPause: function () { 682 if (this.playing) this.pause(); 683 else this.play(); 684 }, 685 play: function () { 686 var ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent); 687 // On iOS this interaction will trigger loading the mp3, so run `init()`. 688 if (ios && this.element.readyState == 0) this.init.apply(this); 689 // If the audio hasn't started preloading, then start it now. 690 // Then set `preload` to `true`, so that any tracks loaded in subsequently are loaded straight away. 691 if (!this.settings.preload) { 692 this.settings.preload = true; 693 this.element.setAttribute('preload', 'auto'); 694 container[audiojs].events.trackLoadProgress(this); 695 } 696 this.playing = true; 697 this.element.play(); 698 this.settings.play.apply(this); 699 }, 700 pause: function () { 701 this.playing = false; 702 this.element.pause(); 703 this.settings.pause.apply(this); 704 }, 705 setVolume: function (v) { 706 this.element.volume = v; 707 }, 708 trackEnded: function (e) { 709 this.skipTo.apply(this, [0]); 710 if (!this.settings.loop) this.pause.apply(this); 711 this.settings.trackEnded.apply(this); 712 } 713 }; 714 715 // **getElementsByClassName** 716 // Having to rely on `getElementsByTagName` is pretty inflexible internally, so a modified version of Dustin Diaz's `getElementsByClassName` has been included. 717 // This version cleans things up and prefers the native DOM method if it's available. 718 var getByClass = function (searchClass, node) { 719 var matches = []; 720 node = node || document; 721 722 if (node.getElementsByClassName) { 723 matches = node.getElementsByClassName(searchClass); 724 } else { 725 var i, l, 726 els = node.getElementsByTagName('*'), 727 pattern = new RegExp('(^|\\s)' + searchClass + '(\\s|$)'); 728 729 for (i = 0, l = els.length; i < l; i++) { 730 if (pattern.test(els[i].className)) { 731 matches.push(els[i]); 732 } 733 } 734 } 735 return matches.length > 1 ? matches : matches[0]; 736 }; 737// The global variable names are passed in here and can be changed if they conflict with anything else. 738 return container[audiojs]; 739}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.