/*
SoundManager 2: Javascript Sound for the Web
--------------------------------------------
http://www.schillmania.com/projects/soundmanager2/
Copyright (c) 2008, Scott Schiller. All rights reserved.
Code licensed under the BSD License:
http://www.schillmania.com/projects/soundmanager2/license.txt
V2.1.20080331
*/
function SoundManager(smURL,smID) {
var self = this;
this.version = 'V2.1.20080331';
this.url = (smURL||'soundmanager2.swf');
this.debugMode = true; // enable debugging output (div#soundmanager-debug, OR console if available + configured)
this.useConsole = true; // use firebug/safari console.log()-type debug console if available
this.consoleOnly = false; // if console is being used, do not create/write to #soundmanager-debug
this.nullURL = 'data/null.mp3'; // path to "null" (empty) MP3 file, used to unload sounds
this.defaultOptions = {
'autoLoad': false, // enable automatic loading (otherwise .load() will be called on demand with .play(), the latter being nicer on bandwidth - if you want to .load yourself, you also can)
'stream': true, // allows playing before entire file has loaded (recommended)
'autoPlay': false, // enable playing of file as soon as possible (much faster if "stream" is true)
'onid3': null, // callback function for "ID3 data is added/available"
'onload': null, // callback function for "load finished"
'whileloading': null, // callback function for "download progress update" (X of Y bytes received)
'onplay': null, // callback for "play" start
'whileplaying': null, // callback during play (position update)
'onstop': null, // callback for "user stop"
'onfinish': null, // callback function for "sound finished playing"
'onbeforefinish': null, // callback for "before sound finished playing (at [time])"
'onbeforefinishtime': 5000, // offset (milliseconds) before end of sound to trigger beforefinish (eg. 1000 msec = 1 second)
'onbeforefinishcomplete':null, // function to call when said sound finishes playing
'onjustbeforefinish':null, // callback for [n] msec before end of current sound
'onjustbeforefinishtime':200, // [n] - if not using, set to 0 (or null handler) and event will not fire.
'multiShot': true, // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
'position': null, // offset (milliseconds) to seek to within loaded sound data.
'pan': 0, // "pan" settings, left-to-right, -100 to 100
'volume': 100 // self-explanatory. 0-100, the latter being the max.
}
this.allowPolling = true; // allow flash to poll for status update (required for "while playing", "progress" etc. to work.)
this.enabled = false;
this.o = null;
this.id = (smID||'sm2movie');
this.oMC = null;
this.sounds = [];
this.soundIDs = [];
this.isIE = (navigator.userAgent.match(/MSIE/));
this.isSafari = (navigator.userAgent.match(/safari/i));
this.debugID = 'soundmanager-debug';
this._debugOpen = true;
this._didAppend = false;
this._appendSuccess = false;
this._didInit = false;
this._disabled = false;
this._hasConsole = (typeof console != 'undefined' && typeof console.log != 'undefined');
this._debugLevels = !self.isSafari?['debug','info','warn','error']:['log','log','log','log'];
// --- public methods ---
this.supported = function() {
return (self._didInit && !self._disabled);
}
this.getMovie = function(smID) {
// return self.isIE?window[smID]:document[smID];
return self.isIE?window[smID]:(self.isSafari?document[smID+'-embed']:document.getElementById(smID+'-embed'));
}
this.loadFromXML = function(sXmlUrl) {
try {
self.o._loadFromXML(sXmlUrl);
} catch(e) {
self._failSafely();
return true;
}
}
this.createSound = function(oOptions) {
if (!self._didInit) throw new Error('soundManager.createSound(): Not loaded yet - wait for soundManager.onload() before calling sound-related methods');
if (arguments.length==2) {
// function overloading in JS! :) ..assume simple createSound(id,url) use case
oOptions = {'id':arguments[0],'url':arguments[1]}
}
var thisOptions = self._mergeObjects(oOptions);
self._writeDebug('soundManager.createSound(): "'+thisOptions.id+'" ('+thisOptions.url+')',1);
if (self._idCheck(thisOptions.id,true)) {
self._writeDebug('sound '+thisOptions.id+' already defined - exiting',2);
return self.sounds[thisOptions.id];
}
self.sounds[thisOptions.id] = new SMSound(self,thisOptions);
self.soundIDs[self.soundIDs.length] = thisOptions.id;
try {
self.o._createSound(thisOptions.id,thisOptions.onjustbeforefinishtime);
} catch(e) {
self._failSafely();
return true;
}
if (thisOptions.autoLoad || thisOptions.autoPlay) self.sounds[thisOptions.id].load(thisOptions);
if (thisOptions.autoPlay) self.sounds[thisOptions.id].playState = 1; // we can only assume this sound will be playing soon.
return self.sounds[thisOptions.id];
}
this.destroySound = function(sID) {
// explicitly destroy a sound before normal page unload, etc.
if (!self._idCheck(sID)) return false;
for (var i=0; iWarning: soundManager.onload() is undefined.',2);
}
this.onerror = function() {
// stub for user handler, called when SM2 fails to load/init
}
// --- "private" methods ---
this._idCheck = this.getSoundById;
this._disableObject = function(o) {
for (var oProp in o) {
if (typeof o[oProp] == 'function' && typeof o[oProp]._protected == 'undefined') o[oProp] = function(){return false;}
}
oProp = null;
}
this._failSafely = function() {
// exception handler for "object doesn't support this property or method"
var flashCPLink = 'http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html';
var fpgssTitle = 'You may need to whitelist this location/domain eg. file:///C:/ or C:/ or mysite.com, or set ALWAYS ALLOW under the Flash Player Global Security Settings page. Note that this seems to apply only to file system viewing.';
var flashCPL = 'view/edit';
var FPGSS = 'FPGSS';
if (!self._disabled) {
self._writeDebug('soundManager: JS->Flash communication failed. Possible causes: flash/browser security restrictions ('+flashCPL+'), insufficient browser/plugin support, or .swf not found',2);
self._writeDebug('Verify that the movie path of '+self.url+' is correct (test link)',1);
if (self._didAppend) {
if (!document.domain) {
self._writeDebug('Loading from local file system? (document.domain appears to be null, this URL path may need to be added to \'trusted locations\' in '+FPGSS+')',1);
self._writeDebug('Possible security/domain restrictions ('+flashCPL+'), should work when served by http on same domain',1);
}
// self._writeDebug('Note: Movie added via JS method, static object/embed in-page markup method may work instead.');
}
self.disable();
}
}
this._createMovie = function(smID,smURL) {
if (self._didAppend && self._appendSuccess) return false; // ignore if already succeeded
if (window.location.href.indexOf('debug=1')+1) self.debugMode = true; // allow force of debug mode via URL
self._didAppend = true;
var html = ['',''];
var toggleElement = '
-
';
var debugHTML = '';
var appXHTML = 'soundManager._createMovie(): appendChild/innerHTML set failed. Serving application/xhtml+xml MIME type? Browser may be enforcing strict rules, not allowing write to innerHTML. (PS: If so, this means your commitment to XML validation is going to break stuff now, because this part isn\'t finished yet. ;))';
var sHTML = '
'+html[self.isIE?0:1]+'
'+(self.debugMode && ((!self._hasConsole||!self.useConsole)||(self.useConsole && self._hasConsole && !self.consoleOnly)) && !document.getElementById(self.debugID)?'x'+debugHTML+toggleElement:'');
var oTarget = (document.body?document.body:document.getElementsByTagName('div')[0]);
if (oTarget) {
self.oMC = document.createElement('div');
self.oMC.className = 'movieContainer';
// "hide" flash movie
self.oMC.style.position = 'absolute';
self.oMC.style.left = '-256px';
self.oMC.style.width = '1px';
self.oMC.style.height = '1px';
try {
oTarget.appendChild(self.oMC);
self.oMC.innerHTML = html[self.isIE?0:1];
self._appendSuccess = true;
} catch(e) {
// may fail under app/xhtml+xml - has yet to be tested
throw new Error(appXHTML);
}
if (!document.getElementById(self.debugID) && ((!self._hasConsole||!self.useConsole)||(self.useConsole && self._hasConsole && !self.consoleOnly))) {
var oDebug = document.createElement('div');
oDebug.id = self.debugID;
oDebug.style.display = (self.debugMode?'block':'none');
if (self.debugMode) {
try {
var oD = document.createElement('div');
oTarget.appendChild(oD);
oD.innerHTML = toggleElement;
} catch(e) {
throw new Error(appXHTML);
}
}
oTarget.appendChild(oDebug);
}
oTarget = null;
}
self._writeDebug('-- SoundManager 2 Version '+self.version.substr(1)+' --',1);
self._writeDebug('soundManager._createMovie(): trying to load '+smURL+'',1);
}
this._writeDebug = function(sText,sType) {
if (!self.debugMode) return false;
if (self._hasConsole && self.useConsole) {
console[self._debugLevels[sType]||'log'](sText); // firebug et al
if (self.useConsoleOnly) return true;
}
var sDID = 'soundmanager-debug';
try {
var o = document.getElementById(sDID);
if (!o) return false;
var p = document.createElement('div');
p.innerHTML = sText;
// o.appendChild(p); // top-to-bottom
o.insertBefore(p,o.firstChild); // bottom-to-top
} catch(e) {
// oh well
}
o = null;
}
this._writeDebug._protected = true;
this._writeDebugAlert = function(sText) { alert(sText); }
if (window.location.href.indexOf('debug=alert')+1 && self.debugMode) {
self._writeDebug = self._writeDebugAlert;
}
this._toggleDebug = function() {
var o = document.getElementById(self.debugID);
var oT = document.getElementById(self.debugID+'-toggle');
if (!o) return false;
if (self._debugOpen) {
// minimize
oT.innerHTML = '+';
o.style.display = 'none';
} else {
oT.innerHTML = '-';
o.style.display = 'block';
}
self._debugOpen = !self._debugOpen;
}
this._toggleDebug._protected = true;
this._debug = function() {
self._writeDebug('soundManager._debug(): sounds by id/url:',0);
for (var i=0,j=self.soundIDs.length; iFlash/ExternalInterface communication fails under non-IE (?!)
}
this.destruct = function() {
if (self.isSafari) {
/* --
Safari 1.3.2 (v312.6)/OSX 10.3.9 and perhaps newer will crash if a sound is actively loading when user exits/refreshes/leaves page
(Apparently related to ExternalInterface making calls to an unloading/unloaded page?)
Unloading sounds (detaching handlers and so on) may help to prevent this
-- */
for (var i=self.soundIDs.length; i--;) {
if (self.sounds[self.soundIDs[i]].readyState == 1) self.sounds[self.soundIDs[i]].unload();
}
}
self.disable();
// self.o = null;
// self.oMC = null;
}
// SMSound (sound object)
function SMSound(oSM,oOptions) {
var self = this;
var sm = oSM;
this.sID = oOptions.id;
this.url = oOptions.url;
this.options = sm._mergeObjects(oOptions);
this.id3 = {
/*
Name/value pairs set via Flash when available - see reference for names:
http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001567.html
(eg., this.id3.songname or this.id3['songname'])
*/
}
self.resetProperties = function(bLoaded) {
self.bytesLoaded = null;
self.bytesTotal = null;
self.position = null;
self.duration = null;
self.durationEstimate = null;
self.loaded = false;
self.loadSuccess = null;
self.playState = 0;
self.paused = false;
self.readyState = 0; // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
self.didBeforeFinish = false;
self.didJustBeforeFinish = false;
}
self.resetProperties();
// --- public methods ---
this.load = function(oOptions) {
self.loaded = false;
self.loadSuccess = null;
self.readyState = 1;
self.playState = (oOptions.autoPlay||false); // if autoPlay, assume "playing" is true (no way to detect when it actually starts in Flash unless onPlay is watched?)
var thisOptions = sm._mergeObjects(oOptions);
if (typeof thisOptions.url == 'undefined') thisOptions.url = self.url;
try {
sm._writeDebug('loading '+thisOptions.url,1);
sm.o._load(self.sID,thisOptions.url,thisOptions.stream,thisOptions.autoPlay,thisOptions.whileloading?1:0);
} catch(e) {
sm._writeDebug('SMSound().load(): JS->Flash communication failed.',2);
}
}
this.unload = function() {
// Flash 8/AS2 can't "close" a stream - fake it by loading an empty MP3
sm._writeDebug('SMSound().unload(): "'+self.sID+'"');
self.setPosition(0); // reset current sound positioning
sm.o._unload(self.sID,sm.nullURL);
// reset load/status flags
self.resetProperties();
}
this.play = function(oOptions) {
if (!oOptions) oOptions = {};
// --- TODO: make event handlers specified via oOptions only apply to this instance of play() (eg. onfinish applies but will reset to default on finish)
if (oOptions.onfinish) self.options.onfinish = oOptions.onfinish;
if (oOptions.onbeforefinish) self.options.onbeforefinish = oOptions.onbeforefinish;
if (oOptions.onjustbeforefinish) self.options.onjustbeforefinish = oOptions.onjustbeforefinish;
// ---
var thisOptions = sm._mergeObjects(oOptions, self.options); // inherit default createSound()-level options
thisOptions = sm._mergeObjects(thisOptions); // merge with default SM2 options
if (self.playState == 1) {
// var allowMulti = typeof oOptions.multiShot!='undefined'?oOptions.multiShot:sm.defaultOptions.multiShot;
var allowMulti = thisOptions.multiShot;
if (!allowMulti) {
sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing? (one-shot)',1);
return false;
} else {
sm._writeDebug('SMSound.play(): "'+self.sID+'" already playing (multi-shot)',1);
}
}
if (!self.loaded) {
if (self.readyState == 0) {
sm._writeDebug('SMSound.play(): .play() before load request. Attempting to load "'+self.sID+'"',1);
// try to get this sound playing ASAP
thisOptions.stream = true;
thisOptions.autoPlay = true;
// TODO: need to investigate when false, double-playing
// if (typeof oOptions.autoPlay=='undefined') thisOptions.autoPlay = true; // only set autoPlay if unspecified here
self.load(thisOptions); // try to get this sound playing ASAP
} else if (self.readyState == 2) {
sm._writeDebug('SMSound.play(): Could not load "'+self.sID+'" - exiting',2);
return false;
} else {
sm._writeDebug('SMSound.play(): "'+self.sID+'" is loading - attempting to play..',1);
}
} else {
sm._writeDebug('SMSound.play(): "'+self.sID+'"');
}
if (self.paused) {
self.resume();
} else {
self.playState = 1;
self.position = (typeof thisOptions.position != 'undefined' && !isNaN(thisOptions.position)?thisOptions.position/1000:0);
if (thisOptions.onplay) thisOptions.onplay.apply(self);
self.setVolume(thisOptions.volume);
self.setPan(thisOptions.pan);
if (!thisOptions.autoPlay) {
// sm._writeDebug('starting sound '+self.sID);
sm.o._start(self.sID,thisOptions.loop||1,self.position); // TODO: verify !autoPlay doesn't cause issue
}
}
}
this.start = this.play; // just for convenience
this.stop = function(bAll) {
if (self.playState == 1) {
self.playState = 0;
self.paused = false;
if (sm.defaultOptions.onstop) sm.defaultOptions.onstop.apply(self);
sm.o._stop(self.sID);
}
}
this.setPosition = function(nMsecOffset) {
// sm._writeDebug('setPosition('+nMsecOffset+')');
self.options.position = nMsecOffset; // update local options
sm.o._setPosition(self.sID,nMsecOffset/1000,self.paused||!self.playState); // if paused or not playing, will not resume (by playing)
}
this.pause = function() {
if (self.paused) return false;
sm._writeDebug('SMSound.pause()');
self.paused = true;
sm.o._pause(self.sID);
}
this.resume = function() {
if (!self.paused) return false;
sm._writeDebug('SMSound.resume()');
self.paused = false;
sm.o._pause(self.sID); // flash method is toggle-based (pause/resume)
}
this.togglePause = function() {
// if playing, pauses - if paused, resumes playing.
sm._writeDebug('SMSound.togglePause()');
if (!self.playState) {
// self.setPosition();
self.play({position:self.position/1000});
return false;
}
if (self.paused) {
sm._writeDebug('SMSound.togglePause(): resuming..');
self.resume();
} else {
sm._writeDebug('SMSound.togglePause(): pausing..');
self.pause();
}
}
this.setPan = function(nPan) {
if (typeof nPan == 'undefined') nPan = 0;
sm.o._setPan(self.sID,nPan);
self.options.pan = nPan;
}
this.setVolume = function(nVol) {
if (typeof nVol == 'undefined') nVol = 100;
sm.o._setVolume(self.sID,nVol);
self.options.volume = nVol;
}
// --- "private" methods called by Flash ---
this._whileloading = function(nBytesLoaded,nBytesTotal,nDuration) {
self.bytesLoaded = nBytesLoaded;
self.bytesTotal = nBytesTotal;
self.duration = nDuration;
self.durationEstimate = parseInt((self.bytesTotal/self.bytesLoaded)*self.duration); // estimate total time (will only be accurate with CBR MP3s.)
if (self.readyState != 3 && self.options.whileloading) self.options.whileloading.apply(self);
// soundManager._writeDebug('duration/durationEst: '+self.duration+' / '+self.durationEstimate);
}
this._onid3 = function(oID3PropNames,oID3Data) {
// oID3PropNames: string array (names)
// ID3Data: string array (data)
sm._writeDebug('SMSound()._onid3(): "'+this.sID+'" ID3 data received.');
var oData = [];
for (var i=0,j=oID3PropNames.length; itest URL]'));
self.loaded = bSuccess;
self.loadSuccess = bSuccess;
self.readyState = bSuccess?3:2;
if (self.options.onload) self.options.onload.apply(self);
}
this._onbeforefinish = function() {
if (!self.didBeforeFinish) {
self.didBeforeFinish = true;
if (self.options.onbeforefinish) self.options.onbeforefinish.apply(self);
}
}
this._onjustbeforefinish = function(msOffset) {
// msOffset: "end of sound" delay actual value (eg. 200 msec, value at event fire time was 187)
if (!self.didJustBeforeFinish) {
self.didJustBeforeFinish = true;
// soundManager._writeDebug('SMSound._onjustbeforefinish()');
if (self.options.onjustbeforefinish) self.options.onjustbeforefinish.apply(self);;
}
}
this._onfinish = function() {
// sound has finished playing
sm._writeDebug('SMSound._onfinish(): "'+self.sID+'"');
self.playState = 0;
self.paused = false;
if (self.options.onfinish) self.options.onfinish.apply(self);
if (self.options.onbeforefinishcomplete) self.options.onbeforefinishcomplete.apply(self);
// reset some state items
self.setPosition(0);
self.didBeforeFinish = false;
self.didJustBeforeFinish = false;
}
}
}
var soundManager = new SoundManager();
// attach onload handler
if (window.addEventListener) {
window.addEventListener('load',soundManager.beginDelayedInit,false);
window.addEventListener('beforeunload',soundManager.destruct,false);
} else if (window.attachEvent) {
window.attachEvent('onload',soundManager.beginInit);
window.attachEvent('beforeunload',soundManager.destruct);
} else {
// no add/attachevent support - safe to assume no JS->Flash either.
soundManager.onerror();
soundManager.disable();
}