audio.js | |
---|---|
A cross-browser javascript shim for html5 audio | (function(audiojs, audiojsInstance, container) { |
Use the path to the audio.js file to create relative paths to the swf and player graphics Remember that some systems (e.g. ruby on rails) append strings like '?1301478336' to asset paths | var path = (function() {
var re = new RegExp('audio(\.min)?\.js.*'),
scripts = document.getElementsByTagName('script');
for (var i = 0, ii = scripts.length; i < ii; i++) {
var path = scripts[i].getAttribute('src');
if(re.test(path)) return path.replace(re, '');
}
})();
|
The audiojs interfaceThis is the global object which provides an interface for creating new | container[audiojs] = {
instanceCount: 0,
instances: {}, |
The markup for the swf. It is injected into the page if there is not support for the | flashSource: '\
<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="$1" width="1" height="1" name="$1" style="position: absolute; left: -1px;"> \
<param name="movie" value="$2?playerInstance='+audiojs+'.instances[\'$1\']&datetime=$3"> \
<param name="allowscriptaccess" value="always"> \
<embed name="$1" src="$2?playerInstance='+audiojs+'.instances[\'$1\']&datetime=$3" width="1" height="1" allowscriptaccess="always"> \
</object>', |
The main settings objectWhere all the default settings are stored. Each of these variables and methods can be overwritten by the user-provided | settings: {
autoplay: false,
loop: false,
preload: true,
imageLocation: path + 'player-graphics.gif',
swfLocation: path + 'audiojs.swf',
useFlash: (function() {
var a = document.createElement('audio');
return !(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
})(),
hasFlash: (function() {
if (navigator.plugins && navigator.plugins.length && navigator.plugins['Shockwave Flash']) {
return true;
} else if (navigator.mimeTypes && navigator.mimeTypes.length) {
var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
return mimeType && mimeType.enabledPlugin;
} else {
try {
var ax = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
return true;
} catch (e) {}
}
return false;
})(), |
The default markup and classes for creating the player: | createPlayer: {
markup: '\
<div class="play-pause"> \
<p class="play"></p> \
<p class="pause"></p> \
<p class="loading"></p> \
<p class="error"></p> \
</div> \
<div class="scrubber"> \
<div class="progress"></div> \
<div class="loaded"></div> \
</div> \
<div class="time"> \
<em class="played">00:00</em>/<strong class="duration">00:00</strong> \
</div> \
<div class="error-message"></div>',
playPauseClass: 'play-pause',
scrubberClass: 'scrubber',
progressClass: 'progress',
loaderClass: 'loaded',
timeClass: 'time',
durationClass: 'duration',
playedClass: 'played',
errorMessageClass: 'error-message',
playingClass: 'playing',
loadingClass: 'loading',
errorClass: 'error'
}, |
The css used by the default player. This is is dynamically injected into a | css: '\
.audiojs audio { position: absolute; left: -1px; } \
.audiojs { width: 460px; height: 36px; background: #404040; overflow: hidden; font-family: monospace; font-size: 12px; \
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)); \
background-image: -moz-linear-gradient(center top, #444 0%, #555 50%, #444 51%, #444 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); \
-o-box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.3); } \
.audiojs .play-pause { width: 25px; height: 40px; padding: 4px 6px; margin: 0px; float: left; overflow: hidden; border-right: 1px solid #000; } \
.audiojs p { display: none; width: 25px; height: 40px; margin: 0px; cursor: pointer; } \
.audiojs .play { display: block; } \
.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; } \
.audiojs .progress { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #ccc; z-index: 1; \
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)); \
background-image: -moz-linear-gradient(center top, #ccc 0%, #ddd 50%, #ccc 51%, #ccc 100%); } \
.audiojs .loaded { position: absolute; top: 0px; left: 0px; height: 14px; width: 0px; background: #000; \
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)); \
background-image: -moz-linear-gradient(center top, #222 0%, #333 50%, #222 51%, #222 100%); } \
.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); } \
.audiojs .time em { padding: 0px 2px 0px 0px; color: #f9f9f9; font-style: normal; } \
.audiojs .time strong { padding: 0px 0px 0px 2px; font-weight: normal; } \
.audiojs .error-message { float: left; display: none; margin: 0px 10px; height: 36px; width: 400px; overflow: hidden; line-height: 36px; white-space: nowrap; color: #fff; \
text-overflow: ellipsis; -o-text-overflow: ellipsis; -icab-text-overflow: ellipsis; -khtml-text-overflow: ellipsis; -moz-text-overflow: ellipsis; -webkit-text-overflow: ellipsis; } \
.audiojs .error-message a { color: #eee; text-decoration: none; padding-bottom: 1px; border-bottom: 1px solid #999; white-space: wrap; } \
\
.audiojs .play { background: url("$1") -2px -1px no-repeat; } \
.audiojs .loading { background: url("$1") -2px -31px no-repeat; } \
.audiojs .error { background: url("$1") -2px -61px no-repeat; } \
.audiojs .pause { background: url("$1") -2px -91px no-repeat; } \
\
.playing .play, .playing .loading, .playing .error { display: none; } \
.playing .pause { display: block; } \
\
.loading .play, .loading .pause, .loading .error { display: none; } \
.loading .loading { display: block; } \
\
.error .time, .error .play, .error .pause, .error .scrubber, .error .loading { display: none; } \
.error .error { display: block; } \
.error .play-pause p { cursor: auto; } \
.error .error-message { display: block; }', |
The default event callbacks: | trackEnded: function(e) {},
flashError: function() {
var player = this.settings.createPlayer,
errorMessage = getByClass(player.errorMessageClass, this.wrapper),
html = 'Missing <a href="http://get.adobe.com/flashplayer/">flash player</a> plugin.';
if (this.mp3) html += ' <a href="'+this.mp3+'">Download audio file</a>.';
container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass);
container[audiojs].helpers.addClass(this.wrapper, player.errorClass);
errorMessage.innerHTML = html;
},
loadError: function(e) {
var player = this.settings.createPlayer,
errorMessage = getByClass(player.errorMessageClass, this.wrapper);
container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass);
container[audiojs].helpers.addClass(this.wrapper, player.errorClass);
errorMessage.innerHTML = 'Error loading: "'+this.mp3+'"';
},
init: function() {
var player = this.settings.createPlayer;
container[audiojs].helpers.addClass(this.wrapper, player.loadingClass);
},
loadStarted: function() {
var player = this.settings.createPlayer,
duration = getByClass(player.durationClass, this.wrapper),
m = Math.floor(this.duration / 60),
s = Math.floor(this.duration % 60);
container[audiojs].helpers.removeClass(this.wrapper, player.loadingClass);
duration.innerHTML = ((m<10?'0':'')+m+':'+(s<10?'0':'')+s);
},
loadProgress: function(percent) {
var player = this.settings.createPlayer,
scrubber = getByClass(player.scrubberClass, this.wrapper),
loaded = getByClass(player.loaderClass, this.wrapper);
loaded.style.width = (scrubber.offsetWidth * percent) + 'px';
},
playPause: function() {
if (this.playing) this.settings.play();
else this.settings.pause();
},
play: function() {
var player = this.settings.createPlayer;
container[audiojs].helpers.addClass(this.wrapper, player.playingClass);
},
pause: function() {
var player = this.settings.createPlayer;
container[audiojs].helpers.removeClass(this.wrapper, player.playingClass);
},
updatePlayhead: function(percent) {
var player = this.settings.createPlayer,
scrubber = getByClass(player.scrubberClass, this.wrapper),
progress = getByClass(player.progressClass, this.wrapper);
progress.style.width = (scrubber.offsetWidth * percent) + 'px';
var played = getByClass(player.playedClass, this.wrapper),
p = this.duration * percent,
m = Math.floor(p / 60),
s = Math.floor(p % 60);
played.innerHTML = ((m<10?'0':'')+m+':'+(s<10?'0':'')+s);
}
}, |
Contructor functions | |
| create: function(element, options) {
var options = options || {}
if (element.length) {
return this.createAll(options, element);
} else {
return this.newInstance(element, options);
}
}, |
| createAll: function(options, elements) {
var audioElements = elements || document.getElementsByTagName('audio'),
instances = []
options = options || {};
for (var i = 0, ii = audioElements.length; i < ii; i++) {
instances.push(this.newInstance(audioElements[i], options));
}
return instances;
}, |
Creating and returning a new instanceThis goes through all the steps required to build out a usable | newInstance: function(element, options) {
var element = element,
s = this.helpers.clone(this.settings),
id = 'audiojs'+this.instanceCount,
wrapperId = 'audiojs_wrapper'+this.instanceCount,
instanceCount = this.instanceCount++; |
Check for | if (element.getAttribute('autoplay') != null) s.autoplay = true;
if (element.getAttribute('loop') != null) s.loop = true;
if (element.getAttribute('preload') == 'none') s.preload = false; |
Merge the default settings with the user-defined | if (options) this.helpers.merge(s, options); |
Inject the player html if required. | if (s.createPlayer.markup) element = this.createPlayer(element, s.createPlayer, wrapperId);
else element.parentNode.setAttribute('id', wrapperId); |
Return a new | var audio = new container[audiojsInstance](element, s); |
If css has been passed in, dynamically inject it into the | if (s.css) this.helpers.injectCss(audio, s.css); |
If | if (s.useFlash && s.hasFlash) {
this.injectFlash(audio, id);
this.attachFlashEvents(audio.wrapper, audio);
} else if (s.useFlash && !s.hasFlash) {
this.settings.flashError.apply(audio);
} |
Attach event callbacks to the new audiojs instance. | if (!s.useFlash || (s.useFlash && s.hasFlash)) this.attachEvents(audio.wrapper, audio); |
Store the newly-created | this.instances[id] = audio;
return audio;
}, |
Helper methods for constructing a working playerInject a wrapping div and the markup for the html player. | createPlayer: function(element, player, id) {
var wrapper = document.createElement('div'),
newElement = element.cloneNode(true);
wrapper.setAttribute('class', 'audiojs');
wrapper.setAttribute('className', 'audiojs');
wrapper.setAttribute('id', id); |
Fix IE's broken implementation of | if (newElement.outerHTML && !document.createElement('audio').canPlayType) {
newElement = this.helpers.cloneHtml5Node(element);
wrapper.innerHTML = player.markup;
wrapper.appendChild(newElement);
element.outerHTML = wrapper.outerHTML;
wrapper = document.getElementById(id);
} else {
wrapper.appendChild(newElement);
wrapper.innerHTML = wrapper.innerHTML + player.markup;
element.parentNode.replaceChild(wrapper, element);
}
return wrapper.getElementsByTagName('audio')[0];
}, |
Attaches useful event callbacks to an | attachEvents: function(wrapper, audio) {
if (!audio.settings.createPlayer) return;
var player = audio.settings.createPlayer,
playPause = getByClass(player.playPauseClass, wrapper),
scrubber = getByClass(player.scrubberClass, wrapper),
leftPos = function(elem) {
var curleft = 0;
if (elem.offsetParent) {
do { curleft += elem.offsetLeft; } while (elem = elem.offsetParent);
}
return curleft;
};
container[audiojs].events.addListener(playPause, 'click', function(e) {
audio.playPause.apply(audio);
});
container[audiojs].events.addListener(scrubber, 'click', function(e) {
var relativeLeft = e.clientX - leftPos(this);
audio.skipTo(relativeLeft / scrubber.offsetWidth);
}); |
If flash is being used, then the following handlers don't need to be registered. | if (audio.settings.useFlash) return; |
Start tracking the load progress of the track. | container[audiojs].events.trackLoadProgress(audio);
container[audiojs].events.addListener(audio.element, 'timeupdate', function(e) {
audio.updatePlayhead.apply(audio);
});
container[audiojs].events.addListener(audio.element, 'ended', function(e) {
audio.trackEnded.apply(audio);
});
container[audiojs].events.addListener(audio.source, 'error', function(e) { |
on error, cancel any load timers that are running. | clearInterval(audio.readyTimer);
clearInterval(audio.loadTimer);
audio.settings.loadError.apply(audio);
});
}, |
Flash requires a slightly different API to the | attachFlashEvents: function(element, audio) {
audio['swfReady'] = false;
audio['load'] = function(mp3) { |
If the swf isn't ready yet then just set | audio.mp3 = mp3;
if (audio.swfReady) audio.element.load(mp3);
}
audio['loadProgress'] = function(percent, duration) {
audio.loadedPercent = percent;
audio.duration = duration;
audio.settings.loadStarted.apply(audio);
audio.settings.loadProgress.apply(audio, [percent]);
}
audio['skipTo'] = function(percent) {
if (percent > audio.loadedPercent) return;
audio.updatePlayhead.call(audio, [percent])
audio.element.skipTo(percent);
}
audio['updatePlayhead'] = function(percent) {
audio.settings.updatePlayhead.apply(audio, [percent]);
}
audio['play'] = function() { |
If the audio hasn't started preloading, then start it now. | if (!audio.settings.preload) {
audio.settings.preload = true;
audio.element.init(audio.mp3);
}
audio.playing = true; |
IE doesn't allow a method named | audio.element.pplay();
audio.settings.play.apply(audio);
}
audio['pause'] = function() {
audio.playing = false; |
Use | audio.element.ppause();
audio.settings.pause.apply(audio);
}
audio['setVolume'] = function(v) {
audio.element.setVolume(v);
}
audio['loadStarted'] = function() { |
Load the mp3 specified by the audio element into the swf. | audio.swfReady = true;
if (audio.settings.preload) audio.element.init(audio.mp3);
if (audio.settings.autoplay) audio.play.apply(audio);
}
}, |
Injecting an swf from a stringBuild up the swf source by replacing the | injectFlash: function(audio, id) {
var flashSource = this.flashSource.replace(/\$1/g, id);
flashSource = flashSource.replace(/\$2/g, audio.settings.swfLocation); |
| flashSource = flashSource.replace(/\$3/g, (+new Date + Math.random())); |
Inject the player markup using a more verbose | var html = audio.wrapper.innerHTML,
div = document.createElement('div');
div.innerHTML = flashSource + html;
audio.wrapper.innerHTML = div.innerHTML;
audio.element = this.helpers.getSwf(id);
}, |
Helper functions | helpers: { |
Merge two objects, with | merge: function(obj1, obj2) {
for (attr in obj2) {
if (obj1.hasOwnProperty(attr) || obj2.hasOwnProperty(attr)) {
obj1[attr] = obj2[attr];
}
}
}, |
Clone a javascript object (recursively) | clone: function(obj){
if (obj == null || typeof(obj) !== 'object') return obj;
var temp = new obj.constructor();
for (var key in obj) temp[key] = arguments.callee(obj[key]);
return temp;
}, |
Adding/removing classnames from elements | addClass: function(element, className) {
var re = new RegExp('(\\s|^)'+className+'(\\s|$)');
if (re.test(element.className)) return;
element.className += ' ' + className;
},
removeClass: function(element, className) {
var re = new RegExp('(\\s|^)'+className+'(\\s|$)');
element.className = element.className.replace(re,' ');
}, |
Dynamic CSS injection | injectCss: function(audio, string) { |
If an | var prepend = '',
styles = document.getElementsByTagName('style'),
css = string.replace(/\$1/g, audio.settings.imageLocation);
for (var i = 0, ii = styles.length; i < ii; i++) {
var title = styles[i].getAttribute('title');
if (title && ~title.indexOf('audiojs')) {
style = styles[i];
if (style.innerHTML === css) return;
prepend = style.innerHTML;
break;
}
};
var head = document.getElementsByTagName('head')[0],
firstchild = head.firstChild,
style = document.createElement('style');
if (!head) return;
style.setAttribute('type', 'text/css');
style.setAttribute('title', 'audiojs');
if (style.styleSheet) style.styleSheet.cssText = prepend + css;
else style.appendChild(document.createTextNode(prepend + css));
if (firstchild) head.insertBefore(style, firstchild);
else head.appendChild(styleElement);
}, |
Handle all the IE6+7 requirements for cloning | cloneHtml5Node: function(audioTag) {
var fragment = document.createDocumentFragment(),
doc = fragment.createElement ? fragment : document;
doc.createElement('audio');
var div = doc.createElement('div');
fragment.appendChild(div);
div.innerHTML = audioTag.outerHTML;
return div.firstChild;
}, |
Cross-browser | getSwf: function(name) {
var swf = document[name] || window[name];
return swf.length > 1 ? swf[swf.length - 1] : swf;
}
}, |
Event-handling | events: {
memoryLeaking: false,
listeners: [], |
A simple cross-browser event handler abstraction | addListener: function(element, eventName, func) { |
For modern browsers use the standard DOM-compliant | if (element.addEventListener) {
element.addEventListener(eventName, func, false); |
For older versions of Internet Explorer, use | } else if (element.attachEvent) {
this.listeners.push(element);
if (!this.memoryLeaking) {
window.attachEvent('onunload', function() {
if(this.listeners) {
for (var i = 0, ii = this.listeners.length; i < ii; i++) {
container[audiojs].events.purge(this.listeners[i]);
}
}
});
this.memoryLeaking = true;
}
element.attachEvent('on' + eventName, function() {
func.call(element, window.event);
});
}
},
trackLoadProgress: function(audio) { |
If | if (!audio.settings.preload) return;
var readyTimer,
loadTimer,
audio = audio,
ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent); |
Use timers here rather than the official | readyTimer = setInterval(function() {
if (audio.element.readyState > -1) { |
iOS doesn't start preloading the mp3 until the user interacts manually, so this stops the loader being displayed prematurely. | if (!ios) audio.init.apply(audio);
}
if (audio.element.readyState > 1) {
if (audio.settings.autoplay) audio.play.apply(audio);
clearInterval(readyTimer); |
Once we have data, start tracking the load progress. | loadTimer = setInterval(function() {
audio.loadProgress.apply(audio);
if (audio.loadedPercent >= 1) clearInterval(loadTimer);
});
}
}, 10);
audio.readyTimer = readyTimer;
audio.loadTimer = loadTimer;
}, |
Douglas Crockford's IE6 memory leak fix | purge: function(d) {
var a = d.attributes, i;
if (a) {
for (i = 0; i < a.length; i += 1) {
if (typeof d[a[i].name] === 'function') d[a[i].name] = null;
}
}
a = d.childNodes;
if (a) {
for (i = 0; i < a.length; i += 1) purge(d.childNodes[i]);
}
}, |
DOMready function | ready: (function() { return function(fn) {
var win = window, done = false, top = true,
doc = win.document, root = doc.documentElement,
add = doc.addEventListener ? 'addEventListener' : 'attachEvent',
rem = doc.addEventListener ? 'removeEventListener' : 'detachEvent',
pre = doc.addEventListener ? '' : 'on',
init = function(e) {
if (e.type == 'readystatechange' && doc.readyState != 'complete') return;
(e.type == 'load' ? win : doc)[rem](pre + e.type, init, false);
if (!done && (done = true)) fn.call(win, e.type || e);
},
poll = function() {
try { root.doScroll('left'); } catch(e) { setTimeout(poll, 50); return; }
init('poll');
};
if (doc.readyState == 'complete') fn.call(win, 'lazy');
else {
if (doc.createEventObject && root.doScroll) {
try { top = !win.frameElement; } catch(e) { }
if (top) poll();
}
doc[add](pre + 'DOMContentLoaded', init, false);
doc[add](pre + 'readystatechange', init, false);
win[add](pre + 'load', init, false);
}
}
})()
}
} |
The audiojs classWe create one of these per | container[audiojsInstance] = function(element, settings) { |
Each audio instance returns an object which contains an API back into the | this.element = element;
this.wrapper = element.parentNode;
this.source = element.getElementsByTagName('source')[0] || element; |
First check the | this.mp3 = (function(element) {
var source = element.getElementsByTagName('source')[0];
return element.getAttribute('src') || (source ? source.getAttribute('src') : null);
})(element);
this.settings = settings;
this.loadStartedCalled = false;
this.loadedPercent = 0;
this.duration = 1;
this.playing = false;
}
container[audiojsInstance].prototype = { |
API access events: Each of these do what they need do and then call the matching methods defined in the settings object. | updatePlayhead: function() {
var percent = this.element.currentTime / this.duration;
this.settings.updatePlayhead.apply(this, [percent]);
},
skipTo: function(percent) {
if (percent > this.loadedPercent) return;
this.element.currentTime = this.duration * percent;
this.updatePlayhead();
},
load: function(mp3) {
this.loadStartedCalled = false;
this.source.setAttribute('src', mp3); |
The now outdated | this.element.load();
this.mp3 = mp3;
container[audiojs].events.trackLoadProgress(this);
},
loadError: function() {
this.settings.loadError.apply(this);
},
init: function() {
this.settings.init.apply(this);
},
loadStarted: function() { |
Wait until | if (!this.element.duration) return false;
this.duration = this.element.duration;
this.updatePlayhead();
this.settings.loadStarted.apply(this);
},
loadProgress: function() {
if (this.element.buffered != null && this.element.buffered.length) { |
Ensure | if (!this.loadStartedCalled) {
this.loadStartedCalled = this.loadStarted();
}
var durationLoaded = this.element.buffered.end(this.element.buffered.length - 1);
this.loadedPercent = durationLoaded / this.duration;
this.settings.loadProgress.apply(this, [this.loadedPercent]);
}
},
playPause: function() {
if (this.playing) this.pause();
else this.play();
},
play: function() {
var ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent); |
On iOS this interaction will trigger loading the mp3, so run | if (ios && this.element.readyState == 0) this.init.apply(this); |
If the audio hasn't started preloading, then start it now. | if (!this.settings.preload) {
this.settings.preload = true;
this.element.setAttribute('preload', 'auto');
container[audiojs].events.trackLoadProgress(this);
}
this.playing = true;
this.element.play();
this.settings.play.apply(this);
},
pause: function() {
this.playing = false;
this.element.pause();
this.settings.pause.apply(this);
},
setVolume: function(v) {
this.element.volume = v;
},
trackEnded: function(e) {
this.skipTo.apply(this, [0]);
if (!this.settings.loop) this.pause.apply(this);
this.settings.trackEnded.apply(this);
}
} |
getElementsByClassName | var getByClass = function(searchClass, node) {
var matches = [];
node = node || document;
if (node.getElementsByClassName) {
matches = node.getElementsByClassName(searchClass);
} else {
var i, l,
els = node.getElementsByTagName("*"),
pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
for (i = 0, l = els.length; i < l; i++) {
if (pattern.test(els[i].className)) {
matches.push(els[i]);
}
}
}
return matches.length > 1 ? matches : matches[0];
}; |
The global variable names are passed in here and can be changed if they conflict with anything else. | })('audiojs', 'audiojsInstance', this);
|