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