UNPKG

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
/** @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 &lt;a&gt; object (or similar object literal) based on audio support. * * @param {object} oLink an HTML DOM &lt;a&gt; 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.');