crossbrowdy
Version:
A Multimedia JavaScript framework to create real cross-platform and hybrid game engines, games, emulators, multimedia libraries and apps.
1,729 lines (1,264 loc) • 164 kB
JavaScript
/** @license
*
* SoundManager 2: JavaScript Sound for the Web
* ----------------------------------------------
* http://schillmania.com/projects/soundmanager2/
*
* Copyright (c) 2007, Scott Schiller. All rights reserved.
* Code provided under the BSD License:
* http://schillmania.com/projects/soundmanager2/license.txt
*
* V2.97a.20150601
*/
/*global window, SM2_DEFER, sm2Debugger, console, document, navigator, setTimeout, setInterval, clearInterval, Audio, opera, module, define */
/*jslint regexp: true, sloppy: true, white: true, nomen: true, plusplus: true, todo: true */
/**
* About this file
* -------------------------------------------------------------------------------------
* This is the fully-commented source version of the SoundManager 2 API,
* recommended for use during development and testing.
*
* See soundmanager2-nodebug-jsmin.js for an optimized build (~11KB with gzip.)
* http://schillmania.com/projects/soundmanager2/doc/getstarted/#basic-inclusion
* Alternately, serve this file with gzip for 75% compression savings (~30KB over HTTP.)
*
* You may notice <d> and </d> comments in this source; these are delimiters for
* debug blocks which are removed in the -nodebug builds, further optimizing code size.
*
* Also, as you may note: Whoa, reliable cross-platform/device audio support is hard! ;)
*/
(function(window, _undefined) {
"use strict";
if (!window || !window.document) {
// Don't cross the [environment] streams. SM2 expects to be running in a browser, not under node.js etc.
// Additionally, if a browser somehow manages to fail this test, as Egon said: "It would be bad."
throw new Error('SoundManager requires a browser with window and document objects.');
}
var soundManager = null;
/**
* The SoundManager constructor.
*
* @constructor
* @param {string} smURL Optional: Path to SWF files
* @param {string} smID Optional: The ID to use for the SWF container element
* @this {SoundManager}
* @return {SoundManager} The new SoundManager instance
*/
function SoundManager(smURL, smID) {
/**
* soundManager configuration options list
* defines top-level configuration properties to be applied to the soundManager instance (eg. soundManager.flashVersion)
* to set these properties, use the setup() method - eg., soundManager.setup({url: '/swf/', flashVersion: 9})
*/
this.setupOptions = {
'url': (smURL || null), // path (directory) where SoundManager 2 SWFs exist, eg., /path/to/swfs/
'flashVersion': 8, // flash build to use (8 or 9.) Some API features require 9.
'debugMode': true, // enable debugging output (console.log() with HTML fallback)
'debugFlash': false, // enable debugging output inside SWF, troubleshoot Flash/browser issues
'useConsole': true, // use console.log() if available (otherwise, writes to #soundmanager-debug element)
'consoleOnly': true, // if console is being used, do not create/write to #soundmanager-debug
'waitForWindowLoad': false, // force SM2 to wait for window.onload() before trying to call soundManager.onload()
'bgColor': '#ffffff', // SWF background color. N/A when wmode = 'transparent'
'useHighPerformance': false, // position:fixed flash movie can help increase js/flash speed, minimize lag
'flashPollingInterval': null, // msec affecting whileplaying/loading callback frequency. If null, default of 50 msec is used.
'html5PollingInterval': null, // msec affecting whileplaying() for HTML5 audio, excluding mobile devices. If null, native HTML5 update events are used.
'flashLoadTimeout': 1000, // msec to wait for flash movie to load before failing (0 = infinity)
'wmode': null, // flash rendering mode - null, 'transparent', or 'opaque' (last two allow z-index to work)
'allowScriptAccess': 'always', // for scripting the SWF (object/embed property), 'always' or 'sameDomain'
'useFlashBlock': false, // *requires flashblock.css, see demos* - allow recovery from flash blockers. Wait indefinitely and apply timeout CSS to SWF, if applicable.
'useHTML5Audio': true, // use HTML5 Audio() where API is supported (most Safari, Chrome versions), Firefox (MP3/MP4 support varies.) Ideally, transparent vs. Flash API where possible.
'forceUseGlobalHTML5Audio': false, // if true, a single Audio() object is used for all sounds - and only one can play at a time.
'ignoreMobileRestrictions': false, // if true, SM2 will not apply global HTML5 audio rules to mobile UAs. iOS > 7 and WebViews may allow multiple Audio() instances.
'html5Test': /^(probably|maybe)$/i, // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative.
'preferFlash': false, // overrides useHTML5audio, will use Flash for MP3/MP4/AAC if present. Potential option if HTML5 playback with these formats is quirky.
'noSWFCache': false, // if true, appends ?ts={date} to break aggressive SWF caching.
'idPrefix': 'sound' // if an id is not provided to createSound(), this prefix is used for generated IDs - 'sound0', 'sound1' etc.
};
this.defaultOptions = {
/**
* the default configuration for sound objects made with createSound() and related methods
* eg., volume, auto-load behaviour and so forth
*/
'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)
'autoPlay': false, // enable playing of file as soon as possible (much faster if "stream" is true)
'from': null, // position to start playback within a sound (msec), default = beginning
'loops': 1, // how many times to repeat the sound (position will wrap around to 0, setPosition() will break out of loop when >0)
'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
'onpause': null, // callback for "pause"
'onresume': null, // callback for "resume" (pause toggle)
'whileplaying': null, // callback during play (position update)
'onposition': null, // object containing times and function callbacks for positions of interest
'onstop': null, // callback for "user stop"
'onfailure': null, // callback function for when playing fails
'onfinish': null, // callback function for "sound finished playing"
'multiShot': true, // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
'multiShotEvents': false, // fire multiple sound events (currently onfinish() only) when multiShot is enabled
'position': null, // offset (milliseconds) to seek to within loaded sound data.
'pan': 0, // "pan" settings, left-to-right, -100 to 100
'stream': true, // allows playing before entire file has loaded (recommended)
'to': null, // position to end playback within a sound (msec), default = end
'type': null, // MIME-like hint for file pattern / canPlay() tests, eg. audio/mp3
'usePolicyFile': false, // enable crossdomain.xml request for audio on remote domains (for ID3/waveform access)
'volume': 100 // self-explanatory. 0-100, the latter being the max.
};
this.flash9Options = {
/**
* flash 9-only options,
* merged into defaultOptions if flash 9 is being used
*/
'isMovieStar': null, // "MovieStar" MPEG4 audio mode. Null (default) = auto detect MP4, AAC etc. based on URL. true = force on, ignore URL
'usePeakData': false, // enable left/right channel peak (level) data
'useWaveformData': false, // enable sound spectrum (raw waveform data) - NOTE: May increase CPU load.
'useEQData': false, // enable sound EQ (frequency spectrum data) - NOTE: May increase CPU load.
'onbufferchange': null, // callback for "isBuffering" property change
'ondataerror': null // callback for waveform/eq data access error (flash playing audio in other tabs/domains)
};
this.movieStarOptions = {
/**
* flash 9.0r115+ MPEG4 audio options,
* merged into defaultOptions if flash 9+movieStar mode is enabled
*/
'bufferTime': 3, // seconds of data to buffer before playback begins (null = flash default of 0.1 seconds - if AAC playback is gappy, try increasing.)
'serverURL': null, // rtmp: FMS or FMIS server to connect to, required when requesting media via RTMP or one of its variants
'onconnect': null, // rtmp: callback for connection to flash media server
'duration': null // rtmp: song duration (msec)
};
this.audioFormats = {
/**
* determines HTML5 support + flash requirements.
* if no support (via flash and/or HTML5) for a "required" format, SM2 will fail to start.
* flash fallback is used for MP3 or MP4 if HTML5 can't play it (or if preferFlash = true)
*/
'mp3': {
'type': ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'],
'required': true
},
'mp4': {
'related': ['aac','m4a','m4b'], // additional formats under the MP4 container
'type': ['audio/mp4; codecs="mp4a.40.2"', 'audio/aac', 'audio/x-m4a', 'audio/MP4A-LATM', 'audio/mpeg4-generic'],
'required': false
},
'ogg': {
'type': ['audio/ogg; codecs=vorbis'],
'required': false
},
'opus': {
'type': ['audio/ogg; codecs=opus', 'audio/opus'],
'required': false
},
'wav': {
'type': ['audio/wav; codecs="1"', 'audio/wav', 'audio/wave', 'audio/x-wav'],
'required': false
}
};
// HTML attributes (id + class names) for the SWF container
this.movieID = 'sm2-container';
this.id = (smID || 'sm2movie');
this.debugID = 'soundmanager-debug';
this.debugURLParam = /([#?&])debug=1/i;
// dynamic attributes
this.versionNumber = 'V2.97a.20150601';
this.version = null;
this.movieURL = null;
this.altURL = null;
this.swfLoaded = false;
this.enabled = false;
this.oMC = null;
this.sounds = {};
this.soundIDs = [];
this.muted = false;
this.didFlashBlock = false;
this.filePattern = null;
this.filePatterns = {
'flash8': /\.mp3(\?.*)?$/i,
'flash9': /\.mp3(\?.*)?$/i
};
// support indicators, set at init
this.features = {
'buffering': false,
'peakData': false,
'waveformData': false,
'eqData': false,
'movieStar': false
};
// flash sandbox info, used primarily in troubleshooting
this.sandbox = {
// <d>
'type': null,
'types': {
'remote': 'remote (domain-based) rules',
'localWithFile': 'local with file access (no internet access)',
'localWithNetwork': 'local with network (internet access only, no local access)',
'localTrusted': 'local, trusted (local+internet access)'
},
'description': null,
'noRemote': null,
'noLocal': null
// </d>
};
/**
* format support (html5/flash)
* stores canPlayType() results based on audioFormats.
* eg. { mp3: boolean, mp4: boolean }
* treat as read-only.
*/
this.html5 = {
'usingFlash': null // set if/when flash fallback is needed
};
// file type support hash
this.flash = {};
// determined at init time
this.html5Only = false;
// used for special cases (eg. iPad/iPhone/palm OS?)
this.ignoreFlash = false;
/**
* a few private internals (OK, a lot. :D)
*/
var SMSound,
sm2 = this, globalHTML5Audio = null, flash = null, sm = 'soundManager', smc = sm + ': ', h5 = 'HTML5::', id, ua = navigator.userAgent, wl = window.location.href.toString(), doc = document, doNothing, setProperties, init, fV, on_queue = [], debugOpen = true, debugTS, didAppend = false, appendSuccess = false, didInit = false, disabled = false, windowLoaded = false, _wDS, wdCount = 0, initComplete, mixin, assign, extraOptions, addOnEvent, processOnEvents, initUserOnload, delayWaitForEI, waitForEI, rebootIntoHTML5, setVersionInfo, handleFocus, strings, initMovie, domContentLoaded, winOnLoad, didDCLoaded, getDocument, createMovie, catchError, setPolling, initDebug, debugLevels = ['log', 'info', 'warn', 'error'], defaultFlashVersion = 8, disableObject, failSafely, normalizeMovieURL, oRemoved = null, oRemovedHTML = null, str, flashBlockHandler, getSWFCSS, swfCSS, toggleDebug, loopFix, policyFix, complain, idCheck, waitingForEI = false, initPending = false, startTimer, stopTimer, timerExecute, h5TimerCount = 0, h5IntervalTimer = null, parseURL, messages = [],
canIgnoreFlash, needsFlash = null, featureCheck, html5OK, html5CanPlay, html5Ext, html5Unload, domContentLoadedIE, testHTML5, event, slice = Array.prototype.slice, useGlobalHTML5Audio = false, lastGlobalHTML5URL, hasFlash, detectFlash, badSafariFix, html5_events, showSupport, flushMessages, wrapCallback, idCounter = 0, didSetup, msecScale = 1000,
is_iDevice = ua.match(/(ipad|iphone|ipod)/i), isAndroid = ua.match(/android/i), isIE = ua.match(/msie/i),
isWebkit = ua.match(/webkit/i),
isSafari = (ua.match(/safari/i) && !ua.match(/chrome/i)),
isOpera = (ua.match(/opera/i)),
mobileHTML5 = (ua.match(/(mobile|pre\/|xoom)/i) || is_iDevice || isAndroid),
isBadSafari = (!wl.match(/usehtml5audio/i) && !wl.match(/sm2\-ignorebadua/i) && isSafari && !ua.match(/silk/i) && ua.match(/OS X 10_6_([3-7])/i)), // Safari 4 and 5 (excluding Kindle Fire, "Silk") occasionally fail to load/play HTML5 audio on Snow Leopard 10.6.3 through 10.6.7 due to bug(s) in QuickTime X and/or other underlying frameworks. :/ Confirmed bug. https://bugs.webkit.org/show_bug.cgi?id=32159
hasConsole = (window.console !== _undefined && console.log !== _undefined),
isFocused = (doc.hasFocus !== _undefined ? doc.hasFocus() : null),
tryInitOnFocus = (isSafari && (doc.hasFocus === _undefined || !doc.hasFocus())),
okToDisable = !tryInitOnFocus,
flashMIME = /(mp3|mp4|mpa|m4a|m4b)/i,
emptyURL = 'about:blank', // safe URL to unload, or load nothing from (flash 8 + most HTML5 UAs)
emptyWAV = 'data:audio/wave;base64,/UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAD//w==', // tiny WAV for HTML5 unloading
overHTTP = (doc.location ? doc.location.protocol.match(/http/i) : null),
http = (!overHTTP ? 'http:/'+'/' : ''),
// mp3, mp4, aac etc.
netStreamMimeTypes = /^\s*audio\/(?:x-)?(?:mpeg4|aac|flv|mov|mp4||m4v|m4a|m4b|mp4v|3gp|3g2)\s*(?:$|;)/i,
// Flash v9.0r115+ "moviestar" formats
netStreamTypes = ['mpeg4', 'aac', 'flv', 'mov', 'mp4', 'm4v', 'f4v', 'm4a', 'm4b', 'mp4v', '3gp', '3g2'],
netStreamPattern = new RegExp('\\.(' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i');
this.mimePattern = /^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i; // default mp3 set
// use altURL if not "online"
this.useAltURL = !overHTTP;
swfCSS = {
'swfBox': 'sm2-object-box',
'swfDefault': 'movieContainer',
'swfError': 'swf_error', // SWF loaded, but SM2 couldn't start (other error)
'swfTimedout': 'swf_timedout',
'swfLoaded': 'swf_loaded',
'swfUnblocked': 'swf_unblocked', // or loaded OK
'sm2Debug': 'sm2_debug',
'highPerf': 'high_performance',
'flashDebug': 'flash_debug'
};
/**
* basic HTML5 Audio() support test
* try...catch because of IE 9 "not implemented" nonsense
* https://github.com/Modernizr/Modernizr/issues/224
*/
this.hasHTML5 = (function() {
try {
// new Audio(null) for stupid Opera 9.64 case, which throws not_enough_arguments exception otherwise.
return (Audio !== _undefined && (isOpera && opera !== _undefined && opera.version() < 10 ? new Audio(null) : new Audio()).canPlayType !== _undefined);
} catch(e) {
return false;
}
}());
/**
* Public SoundManager API
* -----------------------
*/
/**
* Configures top-level soundManager properties.
*
* @param {object} options Option parameters, eg. { flashVersion: 9, url: '/path/to/swfs/' }
* onready and ontimeout are also accepted parameters. call soundManager.setup() to see the full list.
*/
this.setup = function(options) {
var noURL = (!sm2.url);
// warn if flash options have already been applied
if (options !== _undefined && didInit && needsFlash && sm2.ok() && (options.flashVersion !== _undefined || options.url !== _undefined || options.html5Test !== _undefined)) {
complain(str('setupLate'));
}
// TODO: defer: true?
assign(options);
if (!useGlobalHTML5Audio) {
if (mobileHTML5) {
// force the singleton HTML5 pattern on mobile, by default.
if (!sm2.setupOptions.ignoreMobileRestrictions || sm2.setupOptions.forceUseGlobalHTML5Audio) {
messages.push(strings.globalHTML5);
useGlobalHTML5Audio = true;
}
} else {
// only apply singleton HTML5 on desktop if forced.
if (sm2.setupOptions.forceUseGlobalHTML5Audio) {
messages.push(strings.globalHTML5);
useGlobalHTML5Audio = true;
}
}
}
if (!didSetup && mobileHTML5) {
if (sm2.setupOptions.ignoreMobileRestrictions) {
messages.push(strings.ignoreMobile);
} else {
// prefer HTML5 for mobile + tablet-like devices, probably more reliable vs. flash at this point.
// <d>
if (!sm2.setupOptions.useHTML5Audio || sm2.setupOptions.preferFlash) {
// notify that defaults are being changed.
sm2._wD(strings.mobileUA);
}
// </d>
sm2.setupOptions.useHTML5Audio = true;
sm2.setupOptions.preferFlash = false;
if (is_iDevice) {
// no flash here.
sm2.ignoreFlash = true;
} else if ((isAndroid && !ua.match(/android\s2\.3/i)) || !isAndroid) {
/**
* Android devices tend to work better with a single audio instance, specifically for chained playback of sounds in sequence.
* Common use case: exiting sound onfinish() -> createSound() -> play()
* Presuming similar restrictions for other mobile, non-Android, non-iOS devices.
*/
// <d>
sm2._wD(strings.globalHTML5);
// </d>
useGlobalHTML5Audio = true;
}
}
}
// special case 1: "Late setup". SM2 loaded normally, but user didn't assign flash URL eg., setup({url:...}) before SM2 init. Treat as delayed init.
if (options) {
if (noURL && didDCLoaded && options.url !== _undefined) {
sm2.beginDelayedInit();
}
// special case 2: If lazy-loading SM2 (DOMContentLoaded has already happened) and user calls setup() with url: parameter, try to init ASAP.
if (!didDCLoaded && options.url !== _undefined && doc.readyState === 'complete') {
setTimeout(domContentLoaded, 1);
}
}
didSetup = true;
return sm2;
};
this.ok = function() {
return (needsFlash ? (didInit && !disabled) : (sm2.useHTML5Audio && sm2.hasHTML5));
};
this.supported = this.ok; // legacy
this.getMovie = function(smID) {
// safety net: some old browsers differ on SWF references, possibly related to ExternalInterface / flash version
return id(smID) || doc[smID] || window[smID];
};
/**
* Creates a SMSound sound object instance. Can also be overloaded, e.g., createSound('mySound', '/some.mp3');
*
* @param {object} oOptions Sound options (at minimum, url parameter is required.)
* @return {object} SMSound The new SMSound object.
*/
this.createSound = function(oOptions, _url) {
var cs, cs_string, options, oSound = null;
// <d>
cs = sm + '.createSound(): ';
cs_string = cs + str(!didInit ? 'notReady' : 'notOK');
// </d>
if (!didInit || !sm2.ok()) {
complain(cs_string);
return false;
}
if (_url !== _undefined) {
// function overloading in JS! :) ... assume simple createSound(id, url) use case.
oOptions = {
'id': oOptions,
'url': _url
};
}
// inherit from defaultOptions
options = mixin(oOptions);
options.url = parseURL(options.url);
// generate an id, if needed.
if (options.id === _undefined) {
options.id = sm2.setupOptions.idPrefix + (idCounter++);
}
// <d>
if (options.id.toString().charAt(0).match(/^[0-9]$/)) {
sm2._wD(cs + str('badID', options.id), 2);
}
sm2._wD(cs + options.id + (options.url ? ' (' + options.url + ')' : ''), 1);
// </d>
if (idCheck(options.id, true)) {
sm2._wD(cs + options.id + ' exists', 1);
return sm2.sounds[options.id];
}
function make() {
options = loopFix(options);
sm2.sounds[options.id] = new SMSound(options);
sm2.soundIDs.push(options.id);
return sm2.sounds[options.id];
}
if (html5OK(options)) {
oSound = make();
// <d>
if (!sm2.html5Only) {
sm2._wD(options.id + ': Using HTML5');
}
// </d>
oSound._setup_html5(options);
} else {
if (sm2.html5Only) {
sm2._wD(options.id + ': No HTML5 support for this sound, and no Flash. Exiting.');
return make();
}
// TODO: Move HTML5/flash checks into generic URL parsing/handling function.
if (sm2.html5.usingFlash && options.url && options.url.match(/data\:/i)) {
// data: URIs not supported by Flash, either.
sm2._wD(options.id + ': data: URIs not supported via Flash. Exiting.');
return make();
}
if (fV > 8) {
if (options.isMovieStar === null) {
// attempt to detect MPEG-4 formats
options.isMovieStar = !!(options.serverURL || (options.type ? options.type.match(netStreamMimeTypes) : false) || (options.url && options.url.match(netStreamPattern)));
}
// <d>
if (options.isMovieStar) {
sm2._wD(cs + 'using MovieStar handling');
if (options.loops > 1) {
_wDS('noNSLoop');
}
}
// </d>
}
options = policyFix(options, cs);
oSound = make();
if (fV === 8) {
flash._createSound(options.id, options.loops || 1, options.usePolicyFile);
} else {
flash._createSound(options.id, options.url, options.usePeakData, options.useWaveformData, options.useEQData, options.isMovieStar, (options.isMovieStar ? options.bufferTime : false), options.loops || 1, options.serverURL, options.duration || null, options.autoPlay, true, options.autoLoad, options.usePolicyFile);
if (!options.serverURL) {
// We are connected immediately
oSound.connected = true;
if (options.onconnect) {
options.onconnect.apply(oSound);
}
}
}
if (!options.serverURL && (options.autoLoad || options.autoPlay)) {
// call load for non-rtmp streams
oSound.load(options);
}
}
// rtmp will play in onconnect
if (!options.serverURL && options.autoPlay) {
oSound.play();
}
return oSound;
};
/**
* Destroys a SMSound sound object instance.
*
* @param {string} sID The ID of the sound to destroy
*/
this.destroySound = function(sID, _bFromSound) {
// explicitly destroy a sound before normal page unload, etc.
if (!idCheck(sID)) {
return false;
}
var oS = sm2.sounds[sID], i;
oS.stop();
// Disable all callbacks after stop(), when the sound is being destroyed
oS._iO = {};
oS.unload();
for (i = 0; i < sm2.soundIDs.length; i++) {
if (sm2.soundIDs[i] === sID) {
sm2.soundIDs.splice(i, 1);
break;
}
}
if (!_bFromSound) {
// ignore if being called from SMSound instance
oS.destruct(true);
}
oS = null;
delete sm2.sounds[sID];
return true;
};
/**
* Calls the load() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {object} oOptions Optional: Sound options
*/
this.load = function(sID, oOptions) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].load(oOptions);
};
/**
* Calls the unload() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
*/
this.unload = function(sID) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].unload();
};
/**
* Calls the onPosition() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {number} nPosition The position to watch for
* @param {function} oMethod The relevant callback to fire
* @param {object} oScope Optional: The scope to apply the callback to
* @return {SMSound} The SMSound object
*/
this.onPosition = function(sID, nPosition, oMethod, oScope) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].onposition(nPosition, oMethod, oScope);
};
// legacy/backwards-compability: lower-case method name
this.onposition = this.onPosition;
/**
* Calls the clearOnPosition() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {number} nPosition The position to watch for
* @param {function} oMethod Optional: The relevant callback to fire
* @return {SMSound} The SMSound object
*/
this.clearOnPosition = function(sID, nPosition, oMethod) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].clearOnPosition(nPosition, oMethod);
};
/**
* Calls the play() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {object} oOptions Optional: Sound options
* @return {SMSound} The SMSound object
*/
this.play = function(sID, oOptions) {
var result = null,
// legacy function-overloading use case: play('mySound', '/path/to/some.mp3');
overloaded = (oOptions && !(oOptions instanceof Object));
if (!didInit || !sm2.ok()) {
complain(sm + '.play(): ' + str(!didInit?'notReady':'notOK'));
return false;
}
if (!idCheck(sID, overloaded)) {
if (!overloaded) {
// no sound found for the given ID. Bail.
return false;
}
if (overloaded) {
oOptions = {
url: oOptions
};
}
if (oOptions && oOptions.url) {
// overloading use case, create+play: .play('someID', {url:'/path/to.mp3'});
sm2._wD(sm + '.play(): Attempting to create "' + sID + '"', 1);
oOptions.id = sID;
result = sm2.createSound(oOptions).play();
}
} else if (overloaded) {
// existing sound object case
oOptions = {
url: oOptions
};
}
if (result === null) {
// default case
result = sm2.sounds[sID].play(oOptions);
}
return result;
};
// just for convenience
this.start = this.play;
/**
* Calls the setPosition() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {number} nMsecOffset Position (milliseconds)
* @return {SMSound} The SMSound object
*/
this.setPosition = function(sID, nMsecOffset) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].setPosition(nMsecOffset);
};
/**
* Calls the stop() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.stop = function(sID) {
if (!idCheck(sID)) {
return false;
}
sm2._wD(sm + '.stop(' + sID + ')', 1);
return sm2.sounds[sID].stop();
};
/**
* Stops all currently-playing sounds.
*/
this.stopAll = function() {
var oSound;
sm2._wD(sm + '.stopAll()', 1);
for (oSound in sm2.sounds) {
if (sm2.sounds.hasOwnProperty(oSound)) {
// apply only to sound objects
sm2.sounds[oSound].stop();
}
}
};
/**
* Calls the pause() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.pause = function(sID) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].pause();
};
/**
* Pauses all currently-playing sounds.
*/
this.pauseAll = function() {
var i;
for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
sm2.sounds[sm2.soundIDs[i]].pause();
}
};
/**
* Calls the resume() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.resume = function(sID) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].resume();
};
/**
* Resumes all currently-paused sounds.
*/
this.resumeAll = function() {
var i;
for (i = sm2.soundIDs.length- 1 ; i >= 0; i--) {
sm2.sounds[sm2.soundIDs[i]].resume();
}
};
/**
* Calls the togglePause() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.togglePause = function(sID) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].togglePause();
};
/**
* Calls the setPan() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @param {number} nPan The pan value (-100 to 100)
* @return {SMSound} The SMSound object
*/
this.setPan = function(sID, nPan) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].setPan(nPan);
};
/**
* Calls the setVolume() method of a SMSound object by ID
* Overloaded case: pass only volume argument eg., setVolume(50) to apply to all sounds.
*
* @param {string} sID The ID of the sound
* @param {number} nVol The volume value (0 to 100)
* @return {SMSound} The SMSound object
*/
this.setVolume = function(sID, nVol) {
// setVolume(50) function overloading case - apply to all sounds
var i, j;
if (sID !== _undefined && !isNaN(sID) && nVol === _undefined) {
for (i = 0, j = sm2.soundIDs.length; i < j; i++) {
sm2.sounds[sm2.soundIDs[i]].setVolume(sID);
}
return;
}
// setVolume('mySound', 50) case
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].setVolume(nVol);
};
/**
* Calls the mute() method of either a single SMSound object by ID, or all sound objects.
*
* @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
*/
this.mute = function(sID) {
var i = 0;
if (sID instanceof String) {
sID = null;
}
if (!sID) {
sm2._wD(sm + '.mute(): Muting all sounds');
for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
sm2.sounds[sm2.soundIDs[i]].mute();
}
sm2.muted = true;
} else {
if (!idCheck(sID)) {
return false;
}
sm2._wD(sm + '.mute(): Muting "' + sID + '"');
return sm2.sounds[sID].mute();
}
return true;
};
/**
* Mutes all sounds.
*/
this.muteAll = function() {
sm2.mute();
};
/**
* Calls the unmute() method of either a single SMSound object by ID, or all sound objects.
*
* @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
*/
this.unmute = function(sID) {
var i;
if (sID instanceof String) {
sID = null;
}
if (!sID) {
sm2._wD(sm + '.unmute(): Unmuting all sounds');
for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
sm2.sounds[sm2.soundIDs[i]].unmute();
}
sm2.muted = false;
} else {
if (!idCheck(sID)) {
return false;
}
sm2._wD(sm + '.unmute(): Unmuting "' + sID + '"');
return sm2.sounds[sID].unmute();
}
return true;
};
/**
* Unmutes all sounds.
*/
this.unmuteAll = function() {
sm2.unmute();
};
/**
* Calls the toggleMute() method of a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.toggleMute = function(sID) {
if (!idCheck(sID)) {
return false;
}
return sm2.sounds[sID].toggleMute();
};
/**
* Retrieves the memory used by the flash plugin.
*
* @return {number} The amount of memory in use
*/
this.getMemoryUse = function() {
// flash-only
var ram = 0;
if (flash && fV !== 8) {
ram = parseInt(flash._getMemoryUse(), 10);
}
return ram;
};
/**
* Undocumented: NOPs soundManager and all SMSound objects.
*/
this.disable = function(bNoDisable) {
// destroy all functions
var i;
if (bNoDisable === _undefined) {
bNoDisable = false;
}
if (disabled) {
return false;
}
disabled = true;
_wDS('shutdown', 1);
for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
disableObject(sm2.sounds[sm2.soundIDs[i]]);
}
// fire "complete", despite fail
initComplete(bNoDisable);
event.remove(window, 'load', initUserOnload);
return true;
};
/**
* Determines playability of a MIME type, eg. 'audio/mp3'.
*/
this.canPlayMIME = function(sMIME) {
var result;
if (sm2.hasHTML5) {
result = html5CanPlay({
type: sMIME
});
}
if (!result && needsFlash) {
// if flash 9, test netStream (movieStar) types as well.
result = (sMIME && sm2.ok() ? !!((fV > 8 ? sMIME.match(netStreamMimeTypes) : null) || sMIME.match(sm2.mimePattern)) : null); // TODO: make less "weird" (per JSLint)
}
return result;
};
/**
* Determines playability of a URL based on audio support.
*
* @param {string} sURL The URL to test
* @return {boolean} URL playability
*/
this.canPlayURL = function(sURL) {
var result;
if (sm2.hasHTML5) {
result = html5CanPlay({
url: sURL
});
}
if (!result && needsFlash) {
result = (sURL && sm2.ok() ? !!(sURL.match(sm2.filePattern)) : null);
}
return result;
};
/**
* Determines playability of an HTML DOM <a> object (or similar object literal) based on audio support.
*
* @param {object} oLink an HTML DOM <a> object or object literal including href and/or type attributes
* @return {boolean} URL playability
*/
this.canPlayLink = function(oLink) {
if (oLink.type !== _undefined && oLink.type) {
if (sm2.canPlayMIME(oLink.type)) {
return true;
}
}
return sm2.canPlayURL(oLink.href);
};
/**
* Retrieves a SMSound object by ID.
*
* @param {string} sID The ID of the sound
* @return {SMSound} The SMSound object
*/
this.getSoundById = function(sID, _suppressDebug) {
if (!sID) {
return null;
}
var result = sm2.sounds[sID];
// <d>
if (!result && !_suppressDebug) {
sm2._wD(sm + '.getSoundById(): Sound "' + sID + '" not found.', 2);
}
// </d>
return result;
};
/**
* Queues a callback for execution when SoundManager has successfully initialized.
*
* @param {function} oMethod The callback method to fire
* @param {object} oScope Optional: The scope to apply to the callback
*/
this.onready = function(oMethod, oScope) {
var sType = 'onready',
result = false;
if (typeof oMethod === 'function') {
// <d>
if (didInit) {
sm2._wD(str('queue', sType));
}
// </d>
if (!oScope) {
oScope = window;
}
addOnEvent(sType, oMethod, oScope);
processOnEvents();
result = true;
} else {
throw str('needFunction', sType);
}
return result;
};
/**
* Queues a callback for execution when SoundManager has failed to initialize.
*
* @param {function} oMethod The callback method to fire
* @param {object} oScope Optional: The scope to apply to the callback
*/
this.ontimeout = function(oMethod, oScope) {
var sType = 'ontimeout',
result = false;
if (typeof oMethod === 'function') {
// <d>
if (didInit) {
sm2._wD(str('queue', sType));
}
// </d>
if (!oScope) {
oScope = window;
}
addOnEvent(sType, oMethod, oScope);
processOnEvents({type:sType});
result = true;
} else {
throw str('needFunction', sType);
}
return result;
};
/**
* Writes console.log()-style debug output to a console or in-browser element.
* Applies when debugMode = true
*
* @param {string} sText The console message
* @param {object} nType Optional log level (number), or object. Number case: Log type/style where 0 = 'info', 1 = 'warn', 2 = 'error'. Object case: Object to be dumped.
*/
this._writeDebug = function(sText, sTypeOrObject) {
// pseudo-private console.log()-style output
// <d>
var sDID = 'soundmanager-debug', o, oItem;
if (!sm2.setupOptions.debugMode) {
return false;
}
if (hasConsole && sm2.useConsole) {
if (sTypeOrObject && typeof sTypeOrObject === 'object') {
// object passed; dump to console.
console.log(sText, sTypeOrObject);
} else if (debugLevels[sTypeOrObject] !== _undefined) {
console[debugLevels[sTypeOrObject]](sText);
} else {
console.log(sText);
}
if (sm2.consoleOnly) {
return true;
}
}
o = id(sDID);
if (!o) {
return false;
}
oItem = doc.createElement('div');
if (++wdCount % 2 === 0) {
oItem.className = 'sm2-alt';
}
if (sTypeOrObject === _undefined) {
sTypeOrObject = 0;
} else {
sTypeOrObject = parseInt(sTypeOrObject, 10);
}
oItem.appendChild(doc.createTextNode(sText));
if (sTypeOrObject) {
if (sTypeOrObject >= 2) {
oItem.style.fontWeight = 'bold';
}
if (sTypeOrObject === 3) {
oItem.style.color = '#ff3333';
}
}
// top-to-bottom
// o.appendChild(oItem);
// bottom-to-top
o.insertBefore(oItem, o.firstChild);
o = null;
// </d>
return true;
};
// <d>
// last-resort debugging option
if (wl.indexOf('sm2-debug=alert') !== -1) {
this._writeDebug = function(sText) {
window.alert(sText);
};
}
// </d>
// alias
this._wD = this._writeDebug;
/**
* Provides debug / state information on all SMSound objects.
*/
this._debug = function() {
// <d>
var i, j;
_wDS('currentObj', 1);
for (i = 0, j = sm2.soundIDs.length; i < j; i++) {
sm2.sounds[sm2.soundIDs[i]]._debug();
}
// </d>
};
/**
* Restarts and re-initializes the SoundManager instance.
*
* @param {boolean} resetEvents Optional: When true, removes all registered onready and ontimeout event callbacks.
* @param {boolean} excludeInit Options: When true, does not call beginDelayedInit() (which would restart SM2).
* @return {object} soundManager The soundManager instance.
*/
this.reboot = function(resetEvents, excludeInit) {
// reset some (or all) state, and re-init unless otherwise specified.
// <d>
if (sm2.soundIDs.length) {
sm2._wD('Destroying ' + sm2.soundIDs.length + ' SMSound object' + (sm2.soundIDs.length !== 1 ? 's' : '') + '...');
}
// </d>
var i, j, k;
for (i = sm2.soundIDs.length- 1 ; i >= 0; i--) {
sm2.sounds[sm2.soundIDs[i]].destruct();
}
// trash ze flash (remove from the DOM)
if (flash) {
try {
if (isIE) {
oRemovedHTML = flash.innerHTML;
}
oRemoved = flash.parentNode.removeChild(flash);
} catch(e) {
// Remove failed? May be due to flash blockers silently removing the SWF object/embed node from the DOM. Warn and continue.
_wDS('badRemove', 2);
}
}
// actually, force recreate of movie.
oRemovedHTML = oRemoved = needsFlash = flash = null;
sm2.enabled = didDCLoaded = didInit = waitingForEI = initPending = didAppend = appendSuccess = disabled = useGlobalHTML5Audio = sm2.swfLoaded = false;
sm2.soundIDs = [];
sm2.sounds = {};
idCounter = 0;
didSetup = false;
if (!resetEvents) {
// reset callbacks for onready, ontimeout etc. so that they will fire again on re-init
for (i in on_queue) {
if (on_queue.hasOwnProperty(i)) {
for (j = 0, k = on_queue[i].length; j < k; j++) {
on_queue[i][j].fired = false;
}
}
}
} else {
// remove all callbacks entirely
on_queue = [];
}
// <d>
if (!excludeInit) {
sm2._wD(sm + ': Rebooting...');
}
// </d>
// reset HTML5 and flash canPlay test results
sm2.html5 = {
'usingFlash': null
};
sm2.flash = {};
// reset device-specific HTML/flash mode switches
sm2.html5Only = false;
sm2.ignoreFlash = false;
window.setTimeout(function() {
// by default, re-init
if (!excludeInit) {
sm2.beginDelayedInit();
}
}, 20);
return sm2;
};
this.reset = function() {
/**
* Shuts down and restores the SoundManager instance to its original loaded state, without an explicit reboot. All onready/ontimeout handlers are removed.
* After this call, SM2 may be re-initialized via soundManager.beginDelayedInit().
* @return {object} soundManager The soundManager instance.
*/
_wDS('reset');
return sm2.reboot(true, true);
};
/**
* Undocumented: Determines the SM2 flash movie's load progress.
*
* @return {number or null} Percent loaded, or if invalid/unsupported, null.
*/
this.getMoviePercent = function() {
/**
* Interesting syntax notes...
* Flash/ExternalInterface (ActiveX/NPAPI) bridge methods are not typeof "function" nor instanceof Function, but are still valid.
* Additionally, JSLint dislikes ('PercentLoaded' in flash)-style syntax and recommends hasOwnProperty(), which does not work in this case.
* Furthermore, using (flash && flash.PercentLoaded) causes IE to throw "object doesn't support this property or method".
* Thus, 'in' syntax must be used.
*/
return (flash && 'PercentLoaded' in flash ? flash.PercentLoaded() : null); // Yes, JSLint. See nearby comment in source for explanation.
};
/**
* Additional helper for manually invoking SM2's init process after DOM Ready / window.onload().
*/
this.beginDelayedInit = function() {
windowLoaded = true;
domContentLoaded();
setTimeout(function() {
if (initPending) {
return false;
}
createMovie();
initMovie();
initPending = true;
return true;
}, 20);
delayWaitForEI();
};
/**
* Destroys the SoundManager instance and all SMSound instances.
*/
this.destruct = function() {
sm2._wD(sm + '.destruct()');
sm2.disable(true);
};
/**
* SMSound() (sound object) constructor
* ------------------------------------
*
* @param {object} oOptions Sound options (id and url are required attributes)
* @return {SMSound} The new SMSound object
*/
SMSound = function(oOptions) {
var s = this, resetProperties, add_html5_events, remove_html5_events, stop_html5_timer, start_html5_timer, attachOnPosition, onplay_called = false, onPositionItems = [], onPositionFired = 0, detachOnPosition, applyFromTo, lastURL = null, lastHTML5State, urlOmitted;
lastHTML5State = {
// tracks duration + position (time)
duration: null,
time: null
};
this.id = oOptions.id;
// legacy
this.sID = this.id;
this.url = oOptions.url;
this.options = mixin(oOptions);
// per-play-instance-specific options
this.instanceOptions = this.options;
// short alias
this._iO = this.instanceOptions;
// assign property defaults
this.pan = this.options.pan;
this.volume = this.options.volume;
// whether or not this object is using HTML5
this.isHTML5 = false;
// internal HTML5 Audio() object reference
this._a = null;
// for flash 8 special-case createSound() without url, followed by load/play with url case
urlOmitted = (this.url ? false : true);
/**
* SMSound() public methods
* ------------------------
*/
this.id3 = {};
/**
* Writes SMSound object parameters to debug console
*/
this._debug = function() {
// <d>
sm2._wD(s.id + ': Merged options:', s.options);
// </d>
};
/**
* Begins loading a sound per its *url*.
*
* @param {object} oOptions Optional: Sound options
* @return {SMSound} The SMSound object
*/
this.load = function(oOptions) {
var oSound = null, instanceOptions;
if (oOptions !== _undefined) {
s._iO = mixin(oOptions, s.options);
} else {
oOptions = s.options;
s._iO = oOptions;
if (lastURL && lastURL !== s.url) {
_wDS('manURL');
s._iO.url = s.url;
s.url = null;
}
}
if (!s._iO.url) {
s._iO.url = s.url;
}
s._iO.url = parseURL(s._iO.url);
// ensure we're in sync
s.instanceOptions = s._iO;
// local shortcut
instanceOptions = s._iO;
sm2._wD(s.id + ': load (' + instanceOptions.url + ')');
if (!instanceOptions.url && !s.url) {
sm2._wD(s.id + ': load(): url is unassigned. Exiting.', 2);
return s;
}
// <d>
if (!s.isHTML5 && fV === 8 && !s.url && !instanceOptions.autoPlay) {
// flash 8 load() -> play() won't work before onload has fired.
sm2._wD(s.id + ': Flash 8 load() limitation: Wait for onload() before calling play().', 1);
}
// </d>
if (instanceOptions.url === s.url && s.readyState !== 0 && s.readyState !== 2) {
_wDS('onURL', 1);
// if loaded and an onload() exists, fire immediately.
if (s.readyState === 3 && instanceOptions.onload) {
// assume success based on truthy duration.
wrapCallback(s, function() {
instanceOptions.onload.apply(s, [(!!s.duration)]);
});
}
return s;
}
// reset a few state properties
s.loaded = false;
s.readyState = 1;
s.playState = 0;
s.id3 = {};
// TODO: If switching from HTML5 -> flash (or vice versa), stop currently-playing audio.
if (html5OK(instanceOptions)) {
oSound = s._setup_html5(instanceOptions);
if (!oSound._called_load) {
s._html5_canplay = false;
// TODO: review called_load / html5_canplay logic
// if url provided directly to load(), assign it here.
if (s.url !== instanceOptions.url) {
sm2._wD(_wDS('manURL') + ': ' + instanceOptions.url);
s._a.src = instanceOptions.url;
// TODO: review / re-apply all relevant options (volume, loop, onposition etc.)
// reset position for new URL
s.setPosition(0);
}
// given explicit load call, try to preload.
// early HTML5 implementation (non-standard)
s._a.autobuffer = 'auto';
// standard property, values: none / metadata / auto
// reference: http://msdn.microsoft.com/en-us/library/ie/ff974759%28v=vs.85%29.aspx
s._a.preload = 'auto';
s._a._called_load = true;
} else {
sm2._wD(s.id + ': Ignoring request to load again');
}
} else {
if (sm2.html5Only) {
sm2._wD(s.id + ': No flash support. Exiting.');