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