vendor/assets/javascripts/soundmanager2.js in soundmanager-rails-0.1.0 vs vendor/assets/javascripts/soundmanager2.js in soundmanager-rails-0.1.1

- old
+ new

@@ -6,11 +6,11 @@ * * Copyright (c) 2007, Scott Schiller. All rights reserved. * Code provided under the BSD License: * http://schillmania.com/projects/soundmanager2/license.txt * - * V2.97a.20120318 + * V2.97a.20120624 */ /*global window, SM2_DEFER, sm2Debugger, console, document, navigator, setTimeout, setInterval, clearInterval, Audio */ /*jslint regexp: true, sloppy: true, white: true, nomen: true, plusplus: true */ @@ -44,61 +44,38 @@ * @return {SoundManager} The new SoundManager instance */ function SoundManager(smURL, smID) { - // Top-level configuration options + /** + * 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.flashVersion = 8; // flash build to use (8 or 9.) Some API features require 9. - this.debugMode = true; // enable debugging output (console.log() with HTML fallback) - this.debugFlash = false; // enable debugging output inside SWF, troubleshoot Flash/browser issues - this.useConsole = true; // use console.log() if available (otherwise, writes to #soundmanager-debug element) - this.consoleOnly = true; // if console is being used, do not create/write to #soundmanager-debug - this.waitForWindowLoad = false; // force SM2 to wait for window.onload() before trying to call soundManager.onload() - this.bgColor = '#ffffff'; // SWF background color. N/A when wmode = 'transparent' - this.useHighPerformance = false; // position:fixed flash movie can help increase js/flash speed, minimize lag - this.flashPollingInterval = null; // msec affecting whileplaying/loading callback frequency. If null, default of 50 msec is used. - this.html5PollingInterval = null; // msec affecting whileplaying() for HTML5 audio, excluding mobile devices. If null, native HTML5 update events are used. - this.flashLoadTimeout = 1000; // msec to wait for flash movie to load before failing (0 = infinity) - this.wmode = null; // flash rendering mode - null, 'transparent', or 'opaque' (last two allow z-index to work) - this.allowScriptAccess = 'always'; // for scripting the SWF (object/embed property), 'always' or 'sameDomain' - this.useFlashBlock = false; // *requires flashblock.css, see demos* - allow recovery from flash blockers. Wait indefinitely and apply timeout CSS to SWF, if applicable. - this.useHTML5Audio = true; // use HTML5 Audio() where API is supported (most Safari, Chrome versions), Firefox (no MP3/MP4.) Ideally, transparent vs. Flash API where possible. - this.html5Test = /^(probably|maybe)$/i; // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative. - this.preferFlash = true; // overrides useHTML5audio. if true and flash support present, will try to use flash for MP3/MP4 as needed since HTML5 audio support is still quirky in browsers. - this.noSWFCache = false; // if true, appends ?ts={date} to break aggressive SWF caching. + this.setupOptions = { - this.audioFormats = { + '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 (no MP3/MP4.) Ideally, transparent vs. Flash API where possible. + 'html5Test': /^(probably|maybe)$/i, // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative. + 'preferFlash': true, // overrides useHTML5audio. if true and flash support present, will try to use flash for MP3/MP4 as needed since HTML5 audio support is still quirky in browsers. + 'noSWFCache': false // if true, appends ?ts={date} to break aggressive SWF caching. - /** - * 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) - * multiple MIME types may be tested while trying to get a positive canPlayType() response. - */ - - 'mp3': { - 'type': ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'], - 'required': true - }, - - 'mp4': { - 'related': ['aac','m4a'], // 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 - }, - - 'wav': { - 'type': ['audio/wav; codecs="1"', 'audio/wav', 'audio/wave', 'audio/x-wav'], - 'required': false - } - }; this.defaultOptions = { /** @@ -161,24 +138,55 @@ '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) + * multiple MIME types may be tested while trying to get a positive canPlayType() response. + */ + + 'mp3': { + 'type': ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'], + 'required': true + }, + + 'mp4': { + 'related': ['aac','m4a'], // 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 + }, + + '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.20120318'; + this.versionNumber = 'V2.97a.20120624'; this.version = null; this.movieURL = null; - this.url = (smURL || null); this.altURL = null; this.swfLoaded = false; this.enabled = false; this.oMC = null; this.sounds = {}; @@ -248,26 +256,30 @@ this.html5 = { 'usingFlash': null // set if/when flash fallback is needed }; - this.flash = {}; // file type support hash + // file type support hash + this.flash = {}; - this.html5Only = false; // determined at init time - this.ignoreFlash = false; // used for special cases (eg. iPad/iPhone/palm OS?) + // 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, - _s = this, _flash = null, _sm = 'soundManager', _smc = _sm+'::', _h5 = 'HTML5::', _id, _ua = navigator.userAgent, _win = window, _wl = _win.location.href.toString(), _doc = document, _doNothing, _init, _fV, _on_queue = [], _debugOpen = true, _debugTS, _didAppend = false, _appendSuccess = false, _didInit = false, _disabled = false, _windowLoaded = false, _wDS, _wdCount = 0, _initComplete, _mixin, _addOnEvent, _processOnEvents, _initUserOnload, _delayWaitForEI, _waitForEI, _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, + _s = this, _flash = null, _sm = 'soundManager', _smc = _sm+'::', _h5 = 'HTML5::', _id, _ua = navigator.userAgent, _win = window, _wl = _win.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, _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, _needsFlash = null, _featureCheck, _html5OK, _html5CanPlay, _html5Ext, _html5Unload, _domContentLoadedIE, _testHTML5, _event, _slice = Array.prototype.slice, _useGlobalHTML5Audio = false, _hasFlash, _detectFlash, _badSafariFix, _html5_events, _showSupport, - _is_iDevice = _ua.match(/(ipad|iphone|ipod)/i), _is_firefox = _ua.match(/firefox/i), _is_android = _ua.match(/droid/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)), - _likesHTML5 = (_ua.match(/(mobile|pre\/|xoom)/i) || _is_iDevice), + _is_iDevice = _ua.match(/(ipad|iphone|ipod)/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), _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 = (typeof console !== 'undefined' && typeof console.log !== 'undefined'), _isFocused = (typeof _doc.hasFocus !== 'undefined'?_doc.hasFocus():null), _tryInitOnFocus = (_isSafari && typeof _doc.hasFocus === 'undefined'), _okToDisable = !_tryInitOnFocus, _flashMIME = /(mp3|mp4|mpa)/i, + _hasConsole = (typeof console !== 'undefined' && typeof console.log !== 'undefined'), _isFocused = (typeof _doc.hasFocus !== 'undefined'?_doc.hasFocus():null), _tryInitOnFocus = (_isSafari && (typeof _doc.hasFocus === 'undefined' || !_doc.hasFocus())), _okToDisable = !_tryInitOnFocus, _flashMIME = /(mp3|mp4|mpa|m4a)/i, _emptyURL = 'about:blank', // safe URL to unload, or load nothing from (flash 8 + most HTML5 UAs) _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|mp4v|3gp|3g2)\s*(?:$|;)/i, @@ -277,10 +289,11 @@ this.mimePattern = /^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i; // default mp3 set // use altURL if not "online" this.useAltURL = !_overHTTP; + this._global_a = null; _swfCSS = { 'swfBox': 'sm2-object-box', @@ -293,11 +306,11 @@ 'highPerf': 'high_performance', 'flashDebug': 'flash_debug' }; - if (_likesHTML5) { + if (_mobileHTML5) { // prefer HTML5 for mobile + tablet-like devices, probably more reliable vs. flash at this point. _s.useHTML5Audio = true; _s.preferFlash = false; @@ -312,10 +325,33 @@ /** * 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) { + + // warn if flash options have already been applied + + if (typeof options !== 'undefined' && _didInit && _needsFlash && _s.ok() && (typeof options.flashVersion !== 'undefined' || typeof options.url !== 'undefined')) { + _complain(_str('setupLate')); + } + + // TODO: defer: true? + + _assign(options); + + return _s; + + }; + this.ok = function() { return (_needsFlash?(_didInit && !_disabled):(_s.useHTML5Audio && _s.hasHTML5)); }; @@ -330,18 +366,17 @@ }; /** * Creates a SMSound sound object instance. * - * @param {object} oOptions Sound options (at minimum, id and url are required.) + * @param {object} oOptions Sound options (at minimum, id and url parameters are required.) * @return {object} SMSound The new SMSound object. */ - this.createSound = function(oOptions) { + this.createSound = function(oOptions, _url) { - var _cs, _cs_string, - thisOptions = null, oSound = null, _tO = null; + var _cs, _cs_string, thisOptions = null, oSound = null, _tO = null; // <d> _cs = _sm+'.createSound(): '; _cs_string = _cs + _str(!_didInit?'notReady':'notOK'); // </d> @@ -349,15 +384,15 @@ if (!_didInit || !_s.ok()) { _complain(_cs_string); return false; } - if (arguments.length === 2) { + if (typeof _url !== 'undefined') { // function overloading in JS! :) ..assume simple createSound(id,url) use case oOptions = { - 'id': arguments[0], - 'url': arguments[1] + 'id': oOptions, + 'url': _url }; } // inherit from defaultOptions thisOptions = _mixin(oOptions); @@ -390,36 +425,28 @@ } if (_html5OK(_tO)) { oSound = make(); - _s._wD('Loading sound '+_tO.id+' via HTML5'); + _s._wD('Creating sound '+_tO.id+', using HTML5'); oSound._setup_html5(_tO); } else { if (_fV > 8) { if (_tO.isMovieStar === null) { // attempt to detect MPEG-4 formats - _tO.isMovieStar = (_tO.serverURL || (_tO.type ? _tO.type.match(_netStreamMimeTypes) : false) || _tO.url.match(_netStreamPattern)); + _tO.isMovieStar = !!(_tO.serverURL || (_tO.type ? _tO.type.match(_netStreamMimeTypes) : false) || _tO.url.match(_netStreamPattern)); } // <d> if (_tO.isMovieStar) { _s._wD(_cs + 'using MovieStar handling'); - } - // </d> - if (_tO.isMovieStar) { - if (_tO.usePeakData) { - _wDS('noPeak'); - _tO.usePeakData = false; - } - // <d> if (_tO.loops > 1) { _wDS('noNSLoop'); } - // </d> } + // </d> } _tO = _policyFix(_tO, _cs); oSound = make(); @@ -572,13 +599,15 @@ * @return {SMSound} The SMSound object */ this.play = function(sID, oOptions) { + var result = false; + if (!_didInit || !_s.ok()) { _complain(_sm+'.play(): ' + _str(!_didInit?'notReady':'notOK')); - return false; + return result; } if (!_idCheck(sID)) { if (!(oOptions instanceof Object)) { // overloading use case: play('mySound','/path/to/some.mp3'); @@ -588,14 +617,13 @@ } if (oOptions && oOptions.url) { // overloading use case, create+play: .play('someID',{url:'/path/to.mp3'}); _s._wD(_sm+'.play(): attempting to create "' + sID + '"', 1); oOptions.id = sID; - return _s.createSound(oOptions).play(); - } else { - return false; + result = _s.createSound(oOptions).play(); } + return result; } return _s.sounds[sID].play(oOptions); }; @@ -928,18 +956,17 @@ if (_s.hasHTML5) { result = _html5CanPlay({type:sMIME}); } - if (!_needsFlash || result) { - // no flash, or OK - return result; - } else { + if (!result && _needsFlash) { // if flash 9, test netStream (movieStar) types as well. - return (sMIME && _s.ok() ? !!((_fV > 8 ? sMIME.match(_netStreamMimeTypes) : null) || sMIME.match(_s.mimePattern)) : null); + result = (sMIME && _s.ok() ? !!((_fV > 8 ? sMIME.match(_netStreamMimeTypes) : null) || sMIME.match(_s.mimePattern)) : null); } + return result; + }; /** * Determines playability of a URL based on audio support. * @@ -953,17 +980,16 @@ if (_s.hasHTML5) { result = _html5CanPlay({url: sURL}); } - if (!_needsFlash || result) { - // no flash, or OK - return result; - } else { - return (sURL && _s.ok() ? !!(sURL.match(_s.filePattern)) : null); + if (!result && _needsFlash) { + result = (sURL && _s.ok() ? !!(sURL.match(_s.filePattern)) : null); } + return result; + }; /** * Determines playability of an HTML DOM &lt;a&gt; object (or similar object literal) based on audio support. * @@ -1015,13 +1041,14 @@ * @param {object} oScope Optional: The scope to apply to the callback */ this.onready = function(oMethod, oScope) { - var sType = 'onready'; + var sType = 'onready', + result = false; - if (oMethod && oMethod instanceof Function) { + if (typeof oMethod === 'function') { // <d> if (_didInit) { _s._wD(_str('queue', sType)); } @@ -1032,18 +1059,20 @@ } _addOnEvent(sType, oMethod, oScope); _processOnEvents(); - return true; + result = true; } else { throw _str('needFunction', sType); } + return result; + }; /** * Queues a callback for execution when SoundManager has failed to initialize. * @@ -1051,13 +1080,14 @@ * @param {object} oScope Optional: The scope to apply to the callback */ this.ontimeout = function(oMethod, oScope) { - var sType = 'ontimeout'; + var sType = 'ontimeout', + result = false; - if (oMethod && oMethod instanceof Function) { + if (typeof oMethod === 'function') { // <d> if (_didInit) { _s._wD(_str('queue', sType)); } @@ -1068,23 +1098,25 @@ } _addOnEvent(sType, oMethod, oScope); _processOnEvents({type:sType}); - return true; + result = true; } else { throw _str('needFunction', sType); } + return result; + }; /** * Writes console.log()-style debug output to a console or in-browser element. - * Applies when SoundManager.debugMode = true + * Applies when debugMode = true * * @param {string} sText The console message * @param {string} sType Optional: Log type of 'info', 'warn' or 'error' * @param {object} Optional: The scope to apply to the callback */ @@ -1205,19 +1237,21 @@ _s.sounds[_s.soundIDs[i]].destruct(); } // trash ze flash - try { - if (_isIE) { - _oRemovedHTML = _flash.innerHTML; + if (_flash) { + try { + if (_isIE) { + _oRemovedHTML = _flash.innerHTML; + } + _oRemoved = _flash.parentNode.removeChild(_flash); + _s._wD('Flash movie removed.'); + } catch(e) { + // uh-oh. + _wDS('badRemove', 2); } - _oRemoved = _flash.parentNode.removeChild(_flash); - _s._wD('Flash movie removed.'); - } catch(e) { - // uh-oh. - _wDS('badRemove', 2); } // actually, force recreate of movie. _oRemovedHTML = _oRemoved = _needsFlash = null; @@ -1245,10 +1279,11 @@ * @return {number or null} Percent loaded, or if invalid/unsupported, null. */ this.getMoviePercent = function() { + // interesting note: flash/ExternalInterface bridge methods are not typeof "function" nor instanceof Function, but are still valid. return (_flash && typeof _flash.PercentLoaded !== 'undefined' ? _flash.PercentLoaded() : null); }; /** @@ -1305,11 +1340,15 @@ // tracks duration + position (time) duration: null, time: null }; - this.sID = oOptions.id; + 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; @@ -1318,11 +1357,15 @@ 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; /** * SMSound() public methods * ------------------------ @@ -1343,11 +1386,11 @@ var stuff = null, msg = [], sF, sfBracket, maxLength = 64; for (stuff in _t.options) { if (_t.options[stuff] !== null) { - if (_t.options[stuff] instanceof Function) { + if (typeof _t.options[stuff] === 'function') { // handle functions specially sF = _t.options[stuff].toString(); // normalize spaces sF = sF.replace(/\s\s+/g, ' '); sfBracket = sF.indexOf('{'); @@ -1414,54 +1457,78 @@ // local shortcut _iO = _t._iO; _lastURL = _t.url; + + // reset a few state properties + _t.loaded = false; _t.readyState = 1; _t.playState = 0; + _t.id3 = {}; // TODO: If switching from HTML5 -> flash (or vice versa), stop currently-playing audio. if (_html5OK(_iO)) { oS = _t._setup_html5(_iO); if (!oS._called_load) { - _s._wD(_h5+'load: '+_t.sID); + _s._wD(_h5+'load: '+_t.id); + _t._html5_canplay = false; - // given explicit load call, try to get whole file. + // TODO: review called_load / html5_canplay logic + + // if url provided directly to load(), assign it here. + + if (_t._a.src !== _iO.url) { + + _s._wD(_wDS('manURL') + ': ' + _iO.url); + + _t._a.src = _iO.url; + + // TODO: review / re-apply all relevant options (volume, loop, onposition etc.) + + // reset position for new URL + _t.setPosition(0); + + } + + // given explicit load call, try to preload. + // early HTML5 implementation (non-standard) _t._a.autobuffer = 'auto'; // standard _t._a.preload = 'auto'; - oS.load(); oS._called_load = true; if (_iO.autoPlay) { _t.play(); } } else { - _s._wD(_h5+'ignoring request to load again: '+_t.sID); + + _s._wD(_h5+'ignoring request to load again: '+_t.id); + } } else { try { _t.isHTML5 = false; _t._iO = _policyFix(_loopFix(_iO)); // re-assign local shortcut _iO = _t._iO; if (_fV === 8) { - _flash._load(_t.sID, _iO.url, _iO.stream, _iO.autoPlay, (_iO.whileloading?1:0), _iO.loops||1, _iO.usePolicyFile); + _flash._load(_t.id, _iO.url, _iO.stream, _iO.autoPlay, (_iO.whileloading?1:0), _iO.loops||1, _iO.usePolicyFile); } else { - _flash._load(_t.sID, _iO.url, !!(_iO.stream), !!(_iO.autoPlay), _iO.loops||1, !!(_iO.autoLoad), _iO.usePolicyFile); + _flash._load(_t.id, _iO.url, !!(_iO.stream), !!(_iO.autoPlay), _iO.loops||1, !!(_iO.autoLoad), _iO.usePolicyFile); } } catch(e) { _wDS('smError', 2); _debugTS('onload', false); _catchError({type:'SMSOUND_LOAD_JS_EXCEPTION', fatal:true}); @@ -1486,24 +1553,34 @@ // Flash 9/AS3: Close stream, preventing further load // HTML5: Most UAs will use empty URL if (_t.readyState !== 0) { - _s._wD('SMSound.unload(): "' + _t.sID + '"'); + _s._wD('SMSound.unload(): "' + _t.id + '"'); if (!_t.isHTML5) { + if (_fV === 8) { - _flash._unload(_t.sID, _emptyURL); + _flash._unload(_t.id, _emptyURL); } else { - _flash._unload(_t.sID); + _flash._unload(_t.id); } + } else { + _stop_html5_timer(); + if (_t._a) { + _t._a.pause(); - _html5Unload(_t._a); + _html5Unload(_t._a, _emptyURL); + + // reset local URL for next load / play call, too + _t.url = _emptyURL; + } + } // reset load/status flags _resetProperties(); @@ -1517,18 +1594,18 @@ * Unloads and destroys a sound. */ this.destruct = function(_bFromSM) { - _s._wD('SMSound.destruct(): "' + _t.sID + '"'); + _s._wD('SMSound.destruct(): "' + _t.id + '"'); if (!_t.isHTML5) { // kill sound within Flash // Disable the onfailure handler _t._iO.onfailure = null; - _flash._destroySound(_t.sID); + _flash._destroySound(_t.id); } else { _stop_html5_timer(); @@ -1545,11 +1622,11 @@ } if (!_bFromSM) { // ensure deletion from controller - _s.destroySound(_t.sID, true); + _s.destroySound(_t.id, true); } }; @@ -1560,17 +1637,19 @@ * @return {SMSound} The SMSound object */ this.play = function(oOptions, _updatePlayState) { - var fN, allowMulti, a, onready; + var fN, allowMulti, a, onready, startOK = true, + exit = null; // <d> fN = 'SMSound.play(): '; // </d> - _updatePlayState = _updatePlayState === undefined ? true : _updatePlayState; // default to true + // default to true + _updatePlayState = (typeof _updatePlayState === 'undefined' ? true : _updatePlayState); if (!oOptions) { oOptions = {}; } @@ -1595,51 +1674,61 @@ } if (_t.playState === 1 && !_t.paused) { allowMulti = _t._iO.multiShot; if (!allowMulti) { - _s._wD(fN + '"' + _t.sID + '" already playing (one-shot)', 1); - return _t; + _s._wD(fN + '"' + _t.id + '" already playing (one-shot)', 1); + exit = _t; } else { - _s._wD(fN + '"' + _t.sID + '" already playing (multi-shot)', 1); + _s._wD(fN + '"' + _t.id + '" already playing (multi-shot)', 1); } } + if (exit !== null) { + return exit; + } + if (!_t.loaded) { if (_t.readyState === 0) { - _s._wD(fN + 'Attempting to load "' + _t.sID + '"', 1); + _s._wD(fN + 'Attempting to load "' + _t.id + '"', 1); // try to get this sound playing ASAP if (!_t.isHTML5) { // assign directly because setAutoPlay() increments the instanceCount _t._iO.autoPlay = true; + _t.load(_t._iO); + } else { + // iOS needs this when recycling sounds, loading a new URL on an existing object. + _t.load(_t._iO); } - _t.load(_t._iO); - } else if (_t.readyState === 2) { - _s._wD(fN + 'Could not load "' + _t.sID + '" - exiting', 2); - return _t; + _s._wD(fN + 'Could not load "' + _t.id + '" - exiting', 2); + exit = _t; } else { - _s._wD(fN + '"' + _t.sID + '" is loading - attempting to play..', 1); + _s._wD(fN + '"' + _t.id + '" is loading - attempting to play..', 1); } } else { - _s._wD(fN + '"' + _t.sID + '"'); + _s._wD(fN + '"' + _t.id + '"'); } + if (exit !== null) { + return exit; + } + if (!_t.isHTML5 && _fV === 9 && _t.position > 0 && _t.position === _t.duration) { // flash 9 needs a position reset if play() is called while at the end of a sound. - _s._wD(fN + '"' + _t.sID + '": Sound at end, resetting to position:0'); + _s._wD(fN + '"' + _t.id + '": Sound at end, resetting to position:0'); oOptions.position = 0; } /** * Streams will pause when their buffer is full if they are being loaded. @@ -1650,11 +1739,11 @@ */ if (_t.paused && _t.position && _t.position > 0) { // https://gist.github.com/37b17df75cc4d7a90bf6 - _s._wD(fN + '"' + _t.sID + '" is resuming from paused state',1); + _s._wD(fN + '"' + _t.id + '" is resuming from paused state',1); _t.resume(); } else { _t._iO = _mixin(oOptions, _t._iO); @@ -1671,46 +1760,50 @@ // HTML5 needs to at least have "canplay" fired before seeking. if (_t.isHTML5 && !_t._html5_canplay) { // this hasn't been loaded yet. load it first, and then do this again. - _s._wD(fN+'Beginning load of "'+ _t.sID+'" for from/to case'); + _s._wD(fN+'Beginning load of "'+ _t.id+'" for from/to case'); _t.load({ _oncanplay: onready }); - return false; + exit = false; } else if (!_t.isHTML5 && !_t.loaded && (!_t.readyState || _t.readyState !== 2)) { // to be safe, preload the whole thing in Flash. - _s._wD(fN+'Preloading "'+ _t.sID+'" for from/to case'); + _s._wD(fN+'Preloading "'+ _t.id+'" for from/to case'); _t.load({ onload: onready }); - return false; + exit = false; } + if (exit !== null) { + return exit; + } + // otherwise, we're ready to go. re-apply local options, and continue _t._iO = _applyFromTo(); } - _s._wD(fN+'"'+ _t.sID+'" is starting to play'); + _s._wD(fN+'"'+ _t.id+'" is starting to play'); if (!_t.instanceCount || _t._iO.multiShotEvents || (!_t.isHTML5 && _fV > 8 && !_t.getAutoPlay())) { _t.instanceCount++; } // if first play and onposition parameters exist, apply them now - if (_t.playState === 0 && _t._iO.onposition) { + if (_t._iO.onposition && _t.playState === 0) { _attachOnPosition(_t); } _t.playState = 1; _t.paused = false; @@ -1729,17 +1822,31 @@ _t.setVolume(_t._iO.volume, true); _t.setPan(_t._iO.pan, true); if (!_t.isHTML5) { - _flash._start(_t.sID, _t._iO.loops || 1, (_fV === 9?_t._iO.position:_t._iO.position / 1000)); + startOK = _flash._start(_t.id, _t._iO.loops || 1, (_fV === 9 ? _t._iO.position : _t._iO.position / 1000), _t._iO.multiShot); + if (_fV === 9 && !startOK) { + // edge case: no sound hardware, or 32-channel flash ceiling hit. + // applies only to Flash 9, non-NetStream/MovieStar sounds. + // http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/Sound.html#play%28%29 + _s._wD(fN+ _t.id+': No sound hardware, or 32-sound ceiling hit'); + if (_t._iO.onplayerror) { + _t._iO.onplayerror.apply(_t); + } + + } + } else { _start_html5_timer(); + a = _t._setup_html5(); + _t.setPosition(_t._iO.position); + a.play(); } } @@ -1780,11 +1887,11 @@ _t.clearOnPosition(_iO.to); } if (!_t.isHTML5) { - _flash._stop(_t.sID, bAll); + _flash._stop(_t.id, bAll); // hack for netStream: just unload if (_iO.serverURL) { _t.unload(); } @@ -1835,20 +1942,20 @@ * @param {boolean} autoPlay state */ this.setAutoPlay = function(autoPlay) { - _s._wD('sound '+_t.sID+' turned autoplay ' + (autoPlay ? 'on' : 'off')); + _s._wD('sound '+_t.id+' turned autoplay ' + (autoPlay ? 'on' : 'off')); _t._iO.autoPlay = autoPlay; if (!_t.isHTML5) { - _flash._setAutoPlay(_t.sID, autoPlay); + _flash._setAutoPlay(_t.id, autoPlay); if (autoPlay) { // only increment the instanceCount if the sound isn't loaded (TODO: verify RTMP) if (!_t.instanceCount && _t.readyState === 1) { _t.instanceCount++; - _s._wD('sound '+_t.sID+' incremented instance count to '+_t.instanceCount); + _s._wD('sound '+_t.id+' incremented instance count to '+_t.instanceCount); } } } }; @@ -1872,11 +1979,11 @@ * @return {SMSound} The SMSound object */ this.setPosition = function(nMsecOffset) { - if (nMsecOffset === undefined) { + if (typeof nMsecOffset === 'undefined') { nMsecOffset = 0; } var original_pos, position, position1K, @@ -1893,11 +2000,11 @@ if (!_t.isHTML5) { position = (_fV === 9 ? _t.position : position1K); if (_t.readyState && _t.readyState !== 2) { // if paused or not playing, will not resume (by playing) - _flash._setPosition(_t.sID, position, (_t.paused || !_t.playState)); + _flash._setPosition(_t.id, position, (_t.paused || !_t.playState), _t._iO.multiShot); } } else if (_t._a) { // Set the position in the canplay handler if the sound is not ready yet @@ -1951,12 +2058,12 @@ _s._wD('SMSound.pause()'); _t.paused = true; if (!_t.isHTML5) { - if (_bCallFlash || _bCallFlash === undefined) { - _flash._pause(_t.sID); + if (_bCallFlash || typeof _bCallFlash === 'undefined') { + _flash._pause(_t.id, _t._iO.multiShot); } } else { _t._setup_html5().pause(); _stop_html5_timer(); } @@ -1999,11 +2106,11 @@ if (_iO.isMovieStar && !_iO.serverURL) { // Bizarre Webkit bug (Chrome reported via 8tracks.com dudes): AAC content paused for 30+ seconds(?) will not resume without a reposition. _t.setPosition(_t.position); } // flash method is toggle-based (pause/resume) - _flash._pause(_t.sID); + _flash._pause(_t.id, _iO.multiShot); } else { _t._setup_html5().play(); _start_html5_timer(); } @@ -2061,11 +2168,11 @@ if (typeof bInstanceOnly === 'undefined') { bInstanceOnly = false; } if (!_t.isHTML5) { - _flash._setPan(_t.sID, nPan); + _flash._setPan(_t.id, nPan); } // else { no HTML5 pan? } _t._iO.pan = nPan; if (!bInstanceOnly) { @@ -2100,11 +2207,11 @@ if (typeof _bInstanceOnly === 'undefined') { _bInstanceOnly = false; } if (!_t.isHTML5) { - _flash._setVolume(_t.sID, (_s.muted && !_t.muted) || _t.muted?0:nVol); + _flash._setVolume(_t.id, (_s.muted && !_t.muted) || _t.muted?0:nVol); } else if (_t._a) { // valid range: 0-1 _t._a.volume = Math.max(0, Math.min(1, nVol/100)); } @@ -2128,11 +2235,11 @@ this.mute = function() { _t.muted = true; if (!_t.isHTML5) { - _flash._setVolume(_t.sID, 0); + _flash._setVolume(_t.id, 0); } else if (_t._a) { _t._a.muted = true; } return _t; @@ -2146,14 +2253,14 @@ */ this.unmute = function() { _t.muted = false; - var hasIO = typeof _t._iO.volume !== 'undefined'; + var hasIO = (typeof _t._iO.volume !== 'undefined'); if (!_t.isHTML5) { - _flash._setVolume(_t.sID, hasIO?_t._iO.volume:_t.options.volume); + _flash._setVolume(_t.id, hasIO?_t._iO.volume:_t.options.volume); } else if (_t._a) { _t._a.muted = false; } return _t; @@ -2290,11 +2397,11 @@ start, end; end = function() { // end has been reached. - _s._wD(_t.sID + ': "to" time of ' + t + ' reached.'); + _s._wD(_t.id + ': "to" time of ' + t + ' reached.'); // detach listener _t.clearOnPosition(t, end); // stop should clear this, too @@ -2302,11 +2409,11 @@ }; start = function() { - _s._wD(_t.sID + ': playing "from" ' + f); + _s._wD(_t.id + ': playing "from" ' + f); // add listener for end if (t !== null && !isNaN(t)) { _t.onPosition(t, end); } @@ -2382,23 +2489,27 @@ _stopTimer(_t); } }; - _resetProperties = function() { + _resetProperties = function(retainPosition) { - _onPositionItems = []; - _onPositionFired = 0; + if (!retainPosition) { + _onPositionItems = []; + _onPositionFired = 0; + } + _onplay_called = false; _t._hasTimer = null; _t._a = null; _t._html5_canplay = false; _t.bytesLoaded = null; _t.bytesTotal = null; _t.duration = (_t._iO && _t._iO.duration ? _t._iO.duration : null); _t.durationEstimate = null; + _t.buffered = []; // legacy: 1D array _t.eqData = []; _t.eqData.left = []; @@ -2428,10 +2539,12 @@ }; _t.playState = 0; _t.position = null; + _t.id3 = {}; + }; _resetProperties(); /** @@ -2481,20 +2594,20 @@ _t._whileplaying(time,x,x,x,x); } - return isNew; + }/* else { - } else { + // _s._wD('_onTimer: Warn for "'+_t.id+'": '+(!_t._a?'Could not find element. ':'')+(_t.playState === 0?'playState bad, 0?':'playState = '+_t.playState+', OK')); - // _s._wD('_onTimer: Warn for "'+_t.sID+'": '+(!_t._a?'Could not find element. ':'')+(_t.playState === 0?'playState bad, 0?':'playState = '+_t.playState+', OK')); - return false; - } + }*/ + return isNew; + } }; this._get_html5_duration = function() { @@ -2505,65 +2618,107 @@ return result; }; + this._apply_loop = function(a, nLoops) { + + /** + * boolean instead of "loop", for webkit? - spec says string. http://www.w3.org/TR/html-markup/audio.html#audio.attrs.loop + * note that loop is either off or infinite under HTML5, unlike Flash which allows arbitrary loop counts to be specified. + */ + + // <d> + if (!a.loop && nLoops > 1) { + _s._wD('Note: Native HTML5 looping is infinite.'); + } + // </d> + + a.loop = (nLoops > 1 ? 'loop' : ''); + + }; + this._setup_html5 = function(oOptions) { var _iO = _mixin(_t._iO, oOptions), d = decodeURI, _a = _useGlobalHTML5Audio ? _s._global_a : _t._a, _dURL = d(_iO.url), - _oldIO = (_a && _a._t ? _a._t.instanceOptions : null); + _oldIO = (_a && _a._t ? _a._t.instanceOptions : null), + result; if (_a) { if (_a._t) { if (!_useGlobalHTML5Audio && _dURL === d(_lastURL)) { + // same url, ignore request - return _a; + result = _a; + } else if (_useGlobalHTML5Audio && _oldIO.url === _iO.url && (!_lastURL || (_lastURL === _oldIO.url))) { + // iOS-type reuse case - return _a; + result = _a; + } + if (result) { + + _t._apply_loop(_a, _iO.loops); + return result; + + } + } - _s._wD('setting new URL on existing object: ' + _dURL + (_lastURL ? ', old URL: ' + _lastURL : '')); + _s._wD('setting URL on existing object: ' + _dURL + (_lastURL ? ', old URL: ' + _lastURL : '')); /** * "First things first, I, Poppa.." (reset the previous state of the old sound, if playing) * Fixes case with devices that can only play one sound at a time * Otherwise, other sounds in mid-play will be terminated without warning and in a stuck state */ if (_useGlobalHTML5Audio && _a._t && _a._t.playState && _iO.url !== _oldIO.url) { + _a._t.stop(); + } - // new URL, so reset load/playstate and so on - _resetProperties(); + // reset load/playstate, onPosition etc. if the URL is new. + // somewhat-tricky object re-use vs. new SMSound object, old vs. new URL comparisons + _resetProperties((_oldIO && _oldIO.url ? _iO.url === _oldIO.url : (_lastURL ? _lastURL === _iO.url : false))); _a.src = _iO.url; _t.url = _iO.url; _lastURL = _iO.url; _a._called_load = false; } else { - _s._wD('creating HTML5 Audio() element with URL: '+_dURL); - _a = new Audio(_iO.url); + _wDS('h5a'); - _a._called_load = false; + if (_iO.autoLoad || _iO.autoPlay) { - // android (seen in 2.3/Honeycomb) sometimes fails first .load() -> .play(), results in playback failure and ended() events? - if (_is_android) { - _a._called_load = true; + _t._a = new Audio(_iO.url); + + } else { + + // null for stupid Opera 9.64 case + _t._a = (_isOpera ? new Audio(null) : new Audio()); + } + // assign local reference + _a = _t._a; + + _a._called_load = false; + if (_useGlobalHTML5Audio) { + _s._global_a = _a; + } } _t.isHTML5 = true; @@ -2573,29 +2728,27 @@ // store a ref on the audio _a._t = _t; _add_html5_events(); - _a.loop = (_iO.loops>1?'loop':''); + _t._apply_loop(_a, _iO.loops); + if (_iO.autoLoad || _iO.autoPlay) { _t.load(); } else { // early HTML5 implementation (non-standard) _a.autobuffer = false; - // standard - _a.preload = 'none'; + // standard ('none' is also an option.) + _a.preload = 'auto'; } - // boolean instead of "loop", for webkit? - spec says string. http://www.w3.org/TR/html-markup/audio.html#audio.attrs.loop - _a.loop = (_iO.loops > 1 ? 'loop' : ''); - return _a; }; _add_html5_events = function() { @@ -2608,11 +2761,10 @@ function add(oEvt, oFn, bCapture) { return _t._a ? _t._a.addEventListener(oEvt, oFn, bCapture||false) : null; } - _s._wD(_h5+'adding event listeners: '+_t.sID); _t._a._added_events = true; for (f in _html5_events) { if (_html5_events.hasOwnProperty(f)) { add(f, _html5_events[f]); @@ -2631,11 +2783,11 @@ function remove(oEvt, oFn, bCapture) { return (_t._a ? _t._a.removeEventListener(oEvt, oFn, bCapture||false) : null); } - _s._wD(_h5+'removing event listeners: '+_t.sID); + _s._wD(_h5+'removing event listeners: '+_t.id); _t._a._added_events = false; for (f in _html5_events) { if (_html5_events.hasOwnProperty(f)) { remove(f, _html5_events[f]); @@ -2650,15 +2802,17 @@ */ this._onload = function(nSuccess) { - var fN, loadOK = !!(nSuccess); + var fN, + // check for duration to prevent false positives from flash 8 when loading from cache. + loadOK = (!!(nSuccess) || (!_t.isHTML5 && _fV === 8 && _t.duration)); // <d> fN = 'SMSound._onload(): '; - _s._wD(fN + '"' + _t.sID + '"' + (loadOK?' loaded.':' failed to load? - ' + _t.url), (loadOK?1:2)); + _s._wD(fN + '"' + _t.id + '"' + (loadOK?' loaded.':' failed to load? - ' + _t.url), (loadOK?1:2)); if (!loadOK && !_t.isHTML5) { if (_s.sandbox.noRemote === true) { _s._wD(fN + _str('noNet'), 1); } if (_s.sandbox.noLocal === true) { @@ -2722,11 +2876,11 @@ */ this._onfailure = function(msg, level, code) { _t.failures++; - _s._wD('SMSound._onfailure(): "'+_t.sID+'" count '+_t.failures); + _s._wD('SMSound._onfailure(): "'+_t.id+'" count '+_t.failures); if (_t._iO.onfailure && _t.failures === 1) { _t._iO.onfailure(_t, msg, level, code); } else { _s._wD('SMSound._onfailure(): ignoring'); @@ -2758,16 +2912,21 @@ _t.instanceCount = 0; _t.instanceOptions = {}; _t._iO = {}; _stop_html5_timer(); + // reset position, too + if (_t.isHTML5) { + _t.position = 0; + } + } if (!_t.instanceCount || _t._iO.multiShotEvents) { // fire onfinish for last, or every instance if (_io_onfinish) { - _s._wD('SMSound._onfinish(): "' + _t.sID + '"'); + _s._wD('SMSound._onfinish(): "' + _t.id + '"'); _io_onfinish.apply(_t); } } } @@ -2784,34 +2943,39 @@ _t.bufferLength = nBufferLength; if (!_iO.isMovieStar) { if (_iO.duration) { - // use options, if specified and larger + // use duration from options, if specified and larger _t.durationEstimate = (_t.duration > _iO.duration) ? _t.duration : _iO.duration; } else { _t.durationEstimate = parseInt((_t.bytesTotal / _t.bytesLoaded) * _t.duration, 10); - } - if (_t.durationEstimate === undefined) { + if (typeof _t.durationEstimate === 'undefined') { _t.durationEstimate = _t.duration; } - if (_t.readyState !== 3 && _iO.whileloading) { - _iO.whileloading.apply(_t); - } - } else { _t.durationEstimate = _t.duration; - if (_t.readyState !== 3 && _iO.whileloading) { - _iO.whileloading.apply(_t); - } } + // for flash, reflect sequential-load-style buffering + if (!_t.isHTML5) { + _t.buffered = [{ + 'start': 0, + 'end': _t.duration + }]; + } + + // allow whileloading to fire even if "load" fired under HTML5, due to HTTP range/partials + if ((_t.readyState !== 3 || _t.isHTML5) && _iO.whileloading) { + _iO.whileloading.apply(_t); + } + }; this._whileplaying = function(nPosition, oPeakData, oWaveformDataLeft, oWaveformDataRight, oEQData) { var _iO = _t._iO, @@ -2820,11 +2984,13 @@ if (isNaN(nPosition) || nPosition === null) { // flash safety net return false; } - _t.position = nPosition; + // Safari HTML5 play() may return small -ve values when starting from position: 0, eg. -50.120396875. Unexpected/invalid per W3, I think. Normalize to 0. + _t.position = Math.max(0, nPosition); + _t._processOnPosition(); if (!_t.isHTML5 && _fV > 8) { if (_iO.usePeakData && typeof oPeakData !== 'undefined' && oPeakData) { @@ -2870,21 +3036,39 @@ return true; }; + this._oncaptiondata = function(oData) { + + /** + * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature + * + * @param {object} oData + */ + + _s._wD('SMSound._oncaptiondata(): "' + this.id + '" caption data received.'); + + _t.captiondata = oData; + + if (_t._iO.oncaptiondata) { + _t._iO.oncaptiondata.apply(_t); + } + + }; + this._onmetadata = function(oMDProps, oMDData) { /** * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature * RTMP may include song title, MovieStar content may include encoding info * * @param {array} oMDProps (names) * @param {array} oMDData (values) */ - _s._wD('SMSound._onmetadata(): "' + this.sID + '" metadata received.'); + _s._wD('SMSound._onmetadata(): "' + this.id + '" metadata received.'); var oData = {}, i, j; for (i = 0, j = oMDProps.length; i < j; i++) { oData[oMDProps[i]] = oMDData[i]; @@ -2905,11 +3089,11 @@ * * @param {array} oID3Props (names) * @param {array} oID3Data (values) */ - _s._wD('SMSound._onid3(): "' + this.sID + '" ID3 data received.'); + _s._wD('SMSound._onid3(): "' + this.id + '" ID3 data received.'); var oData = [], i, j; for (i = 0, j = oID3Props.length; i < j; i++) { oData[oID3Props[i]] = oID3Data[i]; @@ -2925,18 +3109,18 @@ // flash/RTMP-only this._onconnect = function(bSuccess) { bSuccess = (bSuccess === 1); - _s._wD('SMSound._onconnect(): "'+_t.sID+'"'+(bSuccess?' connected.':' failed to connect? - '+_t.url), (bSuccess?1:2)); + _s._wD('SMSound._onconnect(): "'+_t.id+'"'+(bSuccess?' connected.':' failed to connect? - '+_t.url), (bSuccess?1:2)); _t.connected = bSuccess; if (bSuccess) { _t.failures = 0; - if (_idCheck(_t.sID)) { + if (_idCheck(_t.id)) { if (_t.getAutoPlay()) { // only update the play state if auto playing _t.play(undefined, _t.getAutoPlay()); } else if (_t._iO.autoLoad) { _t.load(); @@ -2984,29 +3168,187 @@ }; _mixin = function(oMain, oAdd) { // non-destructive merge - var o1 = {}, i, o2, o; + var o1 = (oMain || {}), o2, o; - // clone c1 - for (i in oMain) { - if (oMain.hasOwnProperty(i)) { - o1[i] = oMain[i]; - } - } + // if unspecified, o2 is the default options object + o2 = (typeof oAdd === 'undefined' ? _s.defaultOptions : oAdd); - o2 = (typeof oAdd === 'undefined'?_s.defaultOptions:oAdd); for (o in o2) { + if (o2.hasOwnProperty(o) && typeof o1[o] === 'undefined') { - o1[o] = o2[o]; + + if (typeof o2[o] !== 'object' || o2[o] === null) { + + // assign directly + o1[o] = o2[o]; + + } else { + + // recurse through o2 + o1[o] = _mixin(o1[o], o2[o]); + + } + } + } + return o1; }; + // additional soundManager properties that soundManager.setup() will accept + + _extraOptions = { + 'onready': 1, + 'ontimeout': 1, + 'defaultOptions': 1, + 'flash9Options': 1, + 'movieStarOptions': 1 + }; + + _assign = function(o, oParent) { + + /** + * recursive assignment of properties, soundManager.setup() helper + * allows property assignment based on whitelist + */ + + var i, + result = true, + hasParent = (typeof oParent !== 'undefined'), + setupOptions = _s.setupOptions, + extraOptions = _extraOptions; + + // <d> + + // if soundManager.setup() called, show accepted parameters. + + if (typeof o === 'undefined') { + + result = []; + + for (i in setupOptions) { + + if (setupOptions.hasOwnProperty(i)) { + result.push(i); + } + + } + + for (i in extraOptions) { + + if (extraOptions.hasOwnProperty(i)) { + + if (typeof _s[i] === 'object') { + + result.push(i+': {...}'); + + } else if (_s[i] instanceof Function) { + + result.push(i+': function() {...}'); + + } else { + + result.push(i); + + } + + } + + } + + _s._wD(_str('setup', result.join(', '))); + + return false; + + } + + // </d> + + for (i in o) { + + if (o.hasOwnProperty(i)) { + + // if not an {object} we want to recurse through... + + if (typeof o[i] !== 'object' || o[i] === null || o[i] instanceof Array) { + + // check "allowed" options + + if (hasParent && typeof extraOptions[oParent] !== 'undefined') { + + // valid recursive / nested object option, eg., { defaultOptions: { volume: 50 } } + _s[oParent][i] = o[i]; + + } else if (typeof setupOptions[i] !== 'undefined') { + + // special case: assign to setupOptions object, which soundManager property references + _s.setupOptions[i] = o[i]; + + // assign directly to soundManager, too + _s[i] = o[i]; + + } else if (typeof extraOptions[i] === 'undefined') { + + // invalid or disallowed parameter. complain. + _complain(_str((typeof _s[i] === 'undefined' ? 'setupUndef' : 'setupError'), i), 2); + + result = false; + + } else { + + /** + * valid extraOptions parameter. + * is it a method, like onready/ontimeout? call it. + * multiple parameters should be in an array, eg. soundManager.setup({onready: [myHandler, myScope]}); + */ + + if (_s[i] instanceof Function) { + + _s[i].apply(_s, (o[i] instanceof Array? o[i] : [o[i]])); + + } else { + + // good old-fashioned direct assignment + _s[i] = o[i]; + + } + + } + + } else { + + // recursion case, eg., { defaultOptions: { ... } } + + if (typeof extraOptions[i] === 'undefined') { + + // invalid or disallowed parameter. complain. + _complain(_str((typeof _s[i] === 'undefined' ? 'setupUndef' : 'setupError'), i), 2); + + result = false; + + } else { + + // recurse through object + return _assign(o[i], i); + + } + + } + + } + + } + + return result; + + }; + _event = (function() { var old = (_win.attachEvent), evt = { add: (old?'attachEvent':'addEventListener'), @@ -3062,10 +3404,17 @@ 'remove': remove }; }()); + function _preferFlashCheck(kind) { + + // whether flash should play a given type + return (_s.preferFlash && _hasFlash && !_s.ignoreFlash && (typeof _s.flash[kind] !== 'undefined' && _s.flash[kind])); + + } + /** * Internal HTML5 event handling * ----------------------------- */ @@ -3073,36 +3422,39 @@ // wrap html5 event handlers so we don't call them on destroyed sounds return function(e) { - var t = this._t; + var t = this._t, + result; if (!t || !t._a) { // <d> - if (t && t.sID) { - _s._wD(_h5+'ignoring '+e.type+': '+t.sID); + if (t && t.id) { + _s._wD(_h5+'ignoring '+e.type+': '+t.id); } else { _s._wD(_h5+'ignoring '+e.type); } // </d> - return null; + result = null; } else { - return oFn.call(this, e); + result = oFn.call(this, e); } + return result; + }; } _html5_events = { // HTML5 event-name-to-handler map abort: _html5_event(function() { - _s._wD(_h5+'abort: '+this._t.sID); + _s._wD(_h5+'abort: '+this._t.id); }), // enough has loaded to play @@ -3115,58 +3467,59 @@ // this event has already fired. ignore. return true; } t._html5_canplay = true; - _s._wD(_h5+'canplay: '+t.sID+', '+t.url); + _s._wD(_h5+'canplay: '+t.id+', '+t.url); t._onbufferchange(0); - position1K = (!isNaN(t.position)?t.position/1000:null); + // position according to instance options + position1K = (typeof t._iO.position !== 'undefined' && !isNaN(t._iO.position)?t._iO.position/1000:null); + // set the position if position was set before the sound loaded if (t.position && this.currentTime !== position1K) { _s._wD(_h5+'canplay: setting position to '+position1K); try { this.currentTime = position1K; } catch(ee) { - _s._wD(_h5+'setting position failed: '+ee.message, 2); + _s._wD(_h5+'setting position of ' + position1K + ' failed: '+ee.message, 2); } } // hack for HTML5 from/to case if (t._iO._oncanplay) { t._iO._oncanplay(); } }), - load: _html5_event(function() { + canplaythrough: _html5_event(function() { var t = this._t; if (!t.loaded) { t._onbufferchange(0); - // should be 1, and the same - t._whileloading(t.bytesTotal, t.bytesTotal, t._get_html5_duration()); + t._whileloading(t.bytesLoaded, t.bytesTotal, t._get_html5_duration()); t._onload(true); } }), // TODO: Reserved for potential use /* emptied: _html5_event(function() { - _s._wD(_h5+'emptied: '+this._t.sID); + _s._wD(_h5+'emptied: '+this._t.id); }), */ ended: _html5_event(function() { var t = this._t; - _s._wD(_h5+'ended: '+t.sID); + _s._wD(_h5+'ended: '+t.id); t._onfinish(); }), error: _html5_event(function() { @@ -3177,80 +3530,83 @@ }), loadeddata: _html5_event(function() { - var t = this._t, - // at least 1 byte, so math works - bytesTotal = t.bytesTotal || 1; + var t = this._t; - _s._wD(_h5+'loadeddata: '+this._t.sID); + _s._wD(_h5+'loadeddata: '+this._t.id); // safari seems to nicely report progress events, eventually totalling 100% if (!t._loaded && !_isSafari) { t.duration = t._get_html5_duration(); - // fire whileloading() with 100% values - t._whileloading(bytesTotal, bytesTotal, t._get_html5_duration()); - t._onload(true); } }), loadedmetadata: _html5_event(function() { - _s._wD(_h5+'loadedmetadata: '+this._t.sID); + _s._wD(_h5+'loadedmetadata: '+this._t.id); }), loadstart: _html5_event(function() { - _s._wD(_h5+'loadstart: '+this._t.sID); + _s._wD(_h5+'loadstart: '+this._t.id); // assume buffering at first this._t._onbufferchange(1); }), play: _html5_event(function() { - _s._wD(_h5+'play: '+this._t.sID+', '+this._t.url); + _s._wD(_h5+'play: '+this._t.id+', '+this._t.url); // once play starts, no buffering this._t._onbufferchange(0); }), playing: _html5_event(function() { - _s._wD(_h5+'playing: '+this._t.sID); + _s._wD(_h5+'playing: '+this._t.id); // once play starts, no buffering this._t._onbufferchange(0); }), progress: _html5_event(function(e) { + // note: can fire repeatedly after "loaded" event, due to use of HTTP range/partials + var t = this._t, i, j, str, buffered = 0, isProgress = (e.type === 'progress'), ranges = e.target.buffered, // firefox 3.6 implements e.loaded/total (bytes) loaded = (e.loaded||0), total = (e.total||1); - if (t.loaded) { - return false; - } + // reset the "buffered" (loaded byte ranges) array + t.buffered = []; if (ranges && ranges.length) { // if loaded is 0, try TimeRanges implementation as % of load // https://developer.mozilla.org/en/DOM/TimeRanges - for (i=ranges.length-1; i >= 0; i--) { - buffered = (ranges.end(i) - ranges.start(i)); + // re-build "buffered" array + for (i=0, j=ranges.length; i<j; i++) { + t.buffered.push({ + 'start': ranges.start(i), + 'end': ranges.end(i) + }); } + // use the last value locally + buffered = (ranges.end(0) - ranges.start(0)); + // linear case, buffer sum; does not account for seeking and HTTP partials / byte ranges loaded = buffered/e.target.duration; // <d> if (isProgress && ranges.length > 1) { @@ -3261,50 +3617,51 @@ } _s._wD(_h5+'progress: timeRanges: '+str.join(', ')); } if (isProgress && !isNaN(loaded)) { - _s._wD(_h5+'progress: '+t.sID+': ' + Math.floor(loaded*100)+'% loaded'); + _s._wD(_h5+'progress: '+t.id+': ' + Math.floor(loaded*100)+'% loaded'); } // </d> } if (!isNaN(loaded)) { // if progress, likely not buffering t._onbufferchange(0); + // TODO: prevent calls with duplicate values. t._whileloading(loaded, total, t._get_html5_duration()); if (loaded && total && loaded === total) { // in case "onload" doesn't fire (eg. gecko 1.9.2) - _html5_events.load.call(this, e); + _html5_events.canplaythrough.call(this, e); } } }), ratechange: _html5_event(function() { - _s._wD(_h5+'ratechange: '+this._t.sID); + _s._wD(_h5+'ratechange: '+this._t.id); }), suspend: _html5_event(function(e) { // download paused/stopped, may have finished (eg. onload) var t = this._t; - _s._wD(_h5+'suspend: '+t.sID); + _s._wD(_h5+'suspend: '+t.id); _html5_events.progress.call(this, e); t._onsuspend(); }), stalled: _html5_event(function() { - _s._wD(_h5+'stalled: '+this._t.sID); + _s._wD(_h5+'stalled: '+this._t.id); }), timeupdate: _html5_event(function() { @@ -3315,38 +3672,54 @@ waiting: _html5_event(function() { var t = this._t; // see also: seeking - _s._wD(_h5+'waiting: '+t.sID); + _s._wD(_h5+'waiting: '+t.id); // playback faster than download rate, etc. t._onbufferchange(1); }) }; _html5OK = function(iO) { - // Use type, if specified. If HTML5-only mode, no other options, so just give 'er - return (!iO.serverURL && (iO.type?_html5CanPlay({type:iO.type}):_html5CanPlay({url:iO.url})||_s.html5Only)); + // playability test based on URL or MIME type + var result; + + if (iO.serverURL || (iO.type && _preferFlashCheck(iO.type))) { + + // RTMP, or preferring flash + result = false; + + } else { + + // Use type, if specified. If HTML5-only mode, no other options, so just give 'er + result = ((iO.type ? _html5CanPlay({type:iO.type}) : _html5CanPlay({url:iO.url}) || _s.html5Only)); + + } + + return result; + }; - _html5Unload = function(oAudio) { + _html5Unload = function(oAudio, url) { /** * Internal method: Unload media, and cancel any current/pending network requests. * Firefox can load an empty URL, which allegedly destroys the decoder and stops the download. * https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Stopping_the_download_of_media + * However, Firefox has been seen loading a relative URL from '' and thus requesting the hosting page on unload. * Other UA behaviour is unclear, so everyone else gets an about:blank-style URL. */ if (oAudio) { - // Firefox likes '' for unload, most other UAs don't and fail to unload. - oAudio.src = (_is_firefox ? '' : _emptyURL); + // Firefox likes '' for unload (used to work?) - however, may request hosting page URL (bad.) Most other UAs dislike '' and fail to unload. + oAudio.src = url; } }; _html5CanPlay = function(o) { @@ -3369,21 +3742,14 @@ result, offset, fileExt, item; - function preferFlashCheck(kind) { - - // whether flash should play a given type - return (_s.preferFlash && _hasFlash && !_s.ignoreFlash && (typeof _s.flash[kind] !== 'undefined' && _s.flash[kind])); - - } - // account for known cases like audio/mp3 if (mime && typeof _s.html5[mime] !== 'undefined') { - return (_s.html5[mime] && !preferFlashCheck(mime)); + return (_s.html5[mime] && !_preferFlashCheck(mime)); } if (!_html5Ext) { _html5Ext = []; for (item in aF) { @@ -3400,11 +3766,11 @@ // TODO: Strip URL queries, etc. fileExt = (url ? url.toLowerCase().match(_html5Ext) : null); if (!fileExt || !fileExt.length) { if (!mime) { - return false; + result = false; } else { // audio/mp3 -> mp3, result should be known offset = mime.indexOf(';'); // strip "audio/X; codecs.." fileExt = (offset !== -1?mime.substr(0,offset):mime).substr(6); @@ -3414,87 +3780,108 @@ fileExt = fileExt[1]; } if (fileExt && typeof _s.html5[fileExt] !== 'undefined') { // result known - return (_s.html5[fileExt] && !preferFlashCheck(fileExt)); + result = (_s.html5[fileExt] && !_preferFlashCheck(fileExt)); } else { mime = 'audio/'+fileExt; result = _s.html5.canPlayType({type:mime}); _s.html5[fileExt] = result; // _s._wD('canPlayType, found result: '+result); - return (result && _s.html5[mime] && !preferFlashCheck(mime)); + result = (result && _s.html5[mime] && !_preferFlashCheck(mime)); } + return result; + }; _testHTML5 = function() { if (!_s.useHTML5Audio || typeof Audio === 'undefined') { return false; } // double-whammy: Opera 9.64 throws WRONG_ARGUMENTS_ERR if no parameter passed to Audio(), and Webkit + iOS happily tries to load "null" as a URL. :/ var a = (typeof Audio !== 'undefined' ? (_isOpera ? new Audio(null) : new Audio()) : null), - item, support = {}, aF, i; + item, lookup, support = {}, aF, i; function _cp(m) { - var canPlay, i, j, isOK = false; + var canPlay, i, j, + result = false, + isOK = false; if (!a || typeof a.canPlayType !== 'function') { - return false; + return result; } if (m instanceof Array) { // iterate through all mime types, return any successes for (i=0, j=m.length; i<j && !isOK; i++) { if (_s.html5[m[i]] || a.canPlayType(m[i]).match(_s.html5Test)) { isOK = true; _s.html5[m[i]] = true; - - // if flash can play and preferred, also mark it for use. - _s.flash[m[i]] = !!(_s.preferFlash && _hasFlash && m[i].match(_flashMIME)); - + // note flash support, too + _s.flash[m[i]] = !!(m[i].match(_flashMIME)); } } - return isOK; + result = isOK; } else { canPlay = (a && typeof a.canPlayType === 'function' ? a.canPlayType(m) : false); - return !!(canPlay && (canPlay.match(_s.html5Test))); + result = !!(canPlay && (canPlay.match(_s.html5Test))); } + return result; + } // test all registered formats + codecs aF = _s.audioFormats; for (item in aF) { + if (aF.hasOwnProperty(item)) { + + lookup = 'audio/' + item; + support[item] = _cp(aF[item].type); // write back generic type too, eg. audio/mp3 - support['audio/'+item] = support[item]; + support[lookup] = support[item]; // assign flash - if (_s.preferFlash && !_s.ignoreFlash && item.match(_flashMIME)) { + if (item.match(_flashMIME)) { + _s.flash[item] = true; + _s.flash[lookup] = true; + } else { + _s.flash[item] = false; + _s.flash[lookup] = false; + } // assign result to related formats, too + if (aF[item] && aF[item].related) { + for (i=aF[item].related.length-1; i >= 0; i--) { + // eg. audio/m4a support['audio/'+aF[item].related[i]] = support[item]; _s.html5[aF[item].related[i]] = support[item]; _s.flash[aF[item].related[i]] = support[item]; + } + } + } + } support.canPlayType = (a?_cp:null); _s.html5 = _mixin(_s.html5, support); @@ -3503,51 +3890,56 @@ }; _strings = { // <d> - notReady: 'Not loaded yet - wait for soundManager.onload()/onready()', + notReady: 'Not loaded yet - wait for soundManager.onready()', notOK: 'Audio support is not available.', domError: _smc + 'createMovie(): appendChild/innerHTML call failed. DOM not ready or other error.', spcWmode: _smc + 'createMovie(): Removing wmode, preventing known SWF loading issue(s)', swf404: _sm + ': Verify that %s is a valid path.', tryDebug: 'Try ' + _sm + '.debugFlash = true for more security details (output goes to SWF.)', checkSWF: 'See SWF output for more debug info.', localFail: _sm + ': Non-HTTP page (' + _doc.location.protocol + ' URL?) Review Flash player security settings for this special case:\nhttp://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html\nMay need to add/allow path, eg. c:/sm2/ or /users/me/sm2/', - waitFocus: _sm + ': Special case: Waiting for focus-related event..', + waitFocus: _sm + ': Special case: Waiting for SWF to load with window focus...', waitImpatient: _sm + ': Getting impatient, still waiting for Flash%s...', waitForever: _sm + ': Waiting indefinitely for Flash (will recover if unblocked)...', + waitSWF: _sm + ': Retrying, waiting for 100% SWF load...', needFunction: _sm + ': Function object expected for %s', badID: 'Warning: Sound ID "%s" should be a string, starting with a non-numeric character', currentObj: '--- ' + _sm + '._debug(): Current sound objects ---', - waitEI: _smc + 'initMovie(): Waiting for ExternalInterface call from Flash..', + waitEI: _smc + 'initMovie(): Waiting for ExternalInterface call from Flash...', waitOnload: _sm + ': Waiting for window.onload()', docLoaded: _sm + ': Document already loaded', onload: _smc + 'initComplete(): calling soundManager.onload()', onloadOK: _sm + '.onload() complete', init: _smc + 'init()', didInit: _smc + 'init(): Already called?', - flashJS: _sm + ': Attempting to call Flash from JS..', + flashJS: _sm + ': Attempting JS to Flash call...', secNote: 'Flash security note: Network/internet URLs will not load due to security restrictions. Access can be configured via Flash Player Global Security Settings Page: http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html', badRemove: 'Warning: Failed to remove flash movie.', - noPeak: 'Warning: peakData features unsupported for movieStar formats', shutdown: _sm + '.disable(): Shutting down', queue: _sm + ': Queueing %s handler', smFail: _sm + ': Failed to initialise.', smError: 'SMSound.load(): Exception: JS-Flash communication failed, or JS error.', - fbTimeout: 'No flash response, applying .'+_swfCSS.swfTimedout+' CSS..', + fbTimeout: 'No flash response, applying .'+_swfCSS.swfTimedout+' CSS...', fbLoaded: 'Flash loaded', fbHandler: _smc+'flashBlockHandler()', manURL: 'SMSound.load(): Using manually-assigned URL', onURL: _sm + '.load(): current URL already assigned.', badFV: _sm + '.flashVersion must be 8 or 9. "%s" is invalid. Reverting to %s.', as2loop: 'Note: Setting stream:false so looping can work (flash 8 limitation)', noNSLoop: 'Note: Looping not implemented for MovieStar formats', needfl9: 'Note: Switching to flash 9, required for MP4 formats.', mfTimeout: 'Setting flashLoadTimeout = 0 (infinite) for off-screen, mobile flash case', mfOn: 'mobileFlash::enabling on-screen flash repositioning', - policy: 'Enabling usePolicyFile for data access' + policy: 'Enabling usePolicyFile for data access', + setup: _sm + '.setup(): allowed parameters: %s', + setupError: _sm + '.setup(): "%s" cannot be assigned with this method.', + setupUndef: _sm + '.setup(): Could not find option "%s"', + setupLate: _sm + '.setup(): url + flashVersion changes will not take effect until reboot().', + h5a: 'creating HTML5 Audio() object' // </d> }; _str = function() { @@ -3802,15 +4194,11 @@ _idCheck = this.getSoundById; // <d> _wDS = function(o, errorLevel) { - if (!o) { - return ''; - } else { - return _s._wD(_str(o), errorLevel); - } + return (!o ? '' : _s._wD(_str(o), errorLevel)); }; // last-resort debugging option @@ -3936,24 +4324,24 @@ }; _processOnEvents = function(oOptions) { - // assume onready, if unspecified + // if unspecified, assume OK/error if (!oOptions) { oOptions = { - type: 'onready' + type: (_s.ok() ? 'onready' : 'ontimeout') }; } if (!_didInit && oOptions && !oOptions.ignoreInit) { // not ready yet. return false; } - if (oOptions.type === 'ontimeout' && _s.ok()) { + if (oOptions.type === 'ontimeout' && (_s.ok() || (_disabled && !oOptions.ignoreInit))) { // invalid case return false; } var status = { @@ -4006,11 +4394,11 @@ _processOnEvents(); // call user-defined "onload", scoped to window - if (_s.onload instanceof Function) { + if (typeof _s.onload === 'function') { _wDS('onload', 1); _s.onload.apply(_win); _wDS('onloadOK', 1); } @@ -4024,11 +4412,11 @@ _detectFlash = function() { // hat tip: Flash Detect library (BSD, (C) 2007) by Carl "DocYes" S. Yestrau - http://featureblend.com/javascript-flash-detection-library.html / http://featureblend.com/license.txt - if (_hasFlash !== undefined) { + if (typeof _hasFlash !== 'undefined') { // this work has already been done. return _hasFlash; } var hasPlugin = false, n = navigator, nP = n.plugins, obj, type, types, AX = _win.ActiveXObject; @@ -4054,13 +4442,15 @@ }; _featureCheck = function() { - var needsFlash, item, - - // iPhone <= 3.1 has broken HTML5 audio(), but firmware 3.2 (iPad) + iOS4 works. + var needsFlash, + item, + result = true, + formats = _s.audioFormats, + // iPhone <= 3.1 has broken HTML5 audio(), but firmware 3.2 (original iPad) + iOS4 works. isSpecial = (_is_iDevice && !!(_ua.match(/os (1|2|3_0|3_1)/i))); if (isSpecial) { // has Audio(), but is broken; let it load links directly. @@ -4071,46 +4461,47 @@ if (_s.oMC) { _s.oMC.style.display = 'none'; } - return false; + result = false; - } + } else { - if (_s.useHTML5Audio) { + if (_s.useHTML5Audio) { - if (!_s.html5 || !_s.html5.canPlayType) { - _s._wD('SoundManager: No HTML5 Audio() support detected.'); - _s.hasHTML5 = false; - return true; - } else { - _s.hasHTML5 = true; - } - if (_isBadSafari) { - _s._wD(_smc+'Note: Buggy HTML5 Audio in Safari on this OS X release, see https://bugs.webkit.org/show_bug.cgi?id=32159 - '+(!_hasFlash?' would use flash fallback for MP3/MP4, but none detected.':'will use flash fallback for MP3/MP4, if available'),1); - if (_detectFlash()) { - return true; + if (!_s.html5 || !_s.html5.canPlayType) { + _s._wD('SoundManager: No HTML5 Audio() support detected.'); + _s.hasHTML5 = false; + } else { + _s.hasHTML5 = true; } - } - } else { - // flash needed (or, HTML5 needs enabling.) - return true; + // <d> + if (_isBadSafari) { + _s._wD(_smc+'Note: Buggy HTML5 Audio in Safari on this OS X release, see https://bugs.webkit.org/show_bug.cgi?id=32159 - '+(!_hasFlash?' would use flash fallback for MP3/MP4, but none detected.':'will use flash fallback for MP3/MP4, if available'),1); + } + // </d> + } + } - for (item in _s.audioFormats) { - if (_s.audioFormats.hasOwnProperty(item)) { - if ((_s.audioFormats[item].required && !_s.html5.canPlayType(_s.audioFormats[item].type)) || _s.flash[item] || _s.flash[_s.audioFormats[item].type]) { - // flash may be required, or preferred for this format - needsFlash = true; + if (_s.useHTML5Audio && _s.hasHTML5) { + + for (item in formats) { + if (formats.hasOwnProperty(item)) { + if ((formats[item].required && !_s.html5.canPlayType(formats[item].type)) || (_s.preferFlash && (_s.flash[item] || _s.flash[formats[item].type]))) { + // flash may be required, or preferred for this format + needsFlash = true; + } } } + } - // sanity check.. + // sanity check... if (_s.ignoreFlash) { needsFlash = false; } _s.html5Only = (_s.hasHTML5 && _s.useHTML5Audio && !needsFlash); @@ -4124,46 +4515,48 @@ /** * Internal: Finds and returns the first playable URL (or failing that, the first URL.) * @param {string or array} url A single URL string, OR, an array of URL strings or {url:'/path/to/resource', type:'audio/mp3'} objects. */ - var i, j, result = 0; + var i, j, urlResult = 0, result; if (url instanceof Array) { // find the first good one for (i=0, j=url.length; i<j; i++) { if (url[i] instanceof Object) { // MIME check if (_s.canPlayMIME(url[i].type)) { - result = i; + urlResult = i; break; } } else if (_s.canPlayURL(url[i])) { // URL string check - result = i; + urlResult = i; break; } } // normalize to string - if (url[result].url) { - url[result] = url[result].url; + if (url[urlResult].url) { + url[urlResult] = url[urlResult].url; } - return url[result]; + result = url[urlResult]; } else { // single URL case - return url; + result = url; } + return result; + }; _startTimer = function(oSound) { @@ -4173,15 +4566,15 @@ if (!oSound._hasTimer) { oSound._hasTimer = true; - if (!_likesHTML5 && _s.html5PollingInterval) { + if (!_mobileHTML5 && _s.html5PollingInterval) { if (_h5IntervalTimer === null && _h5TimerCount === 0) { - _h5IntervalTimer = window.setInterval(_timerExecute, _s.html5PollingInterval); + _h5IntervalTimer = _win.setInterval(_timerExecute, _s.html5PollingInterval); } _h5TimerCount++; @@ -4199,11 +4592,11 @@ if (oSound._hasTimer) { oSound._hasTimer = false; - if (!_likesHTML5 && _s.html5PollingInterval) { + if (!_mobileHTML5 && _s.html5PollingInterval) { // interval will stop itself at next execution. _h5TimerCount--; @@ -4223,11 +4616,11 @@ if (_h5IntervalTimer !== null && !_h5TimerCount) { // no active timers, stop polling interval. - window.clearInterval(_h5IntervalTimer); + _win.clearInterval(_h5IntervalTimer); _h5IntervalTimer = null; return false; @@ -4249,11 +4642,11 @@ _catchError = function(options) { options = (typeof options !== 'undefined' ? options : {}); - if (_s.onerror instanceof Function) { + if (typeof _s.onerror === 'function') { _s.onerror.apply(_win, [{type:(typeof options.type !== 'undefined' ? options.type : null)}]); } if (typeof options.fatal !== 'undefined' && options.fatal) { _s.disable(); @@ -4361,16 +4754,12 @@ return false; } // </d> - if (_isIE) { - // IE needs a timeout OR delay until window.onload - may need TODO: investigating - setTimeout(_init, 100); - } else { - _init(); - } + // slight delay before init + setTimeout(_init, _isIE ? 100 : 1); }; /** * Private initialization helpers @@ -4409,11 +4798,11 @@ // flash path var remoteURL = (smURL || _s.url), localURL = (_s.altURL || remoteURL), swfTitle = 'JS/Flash audio component (SoundManager 2)', oEmbed, oMovie, oTarget = _getDocument(), tmp, movieHTML, oEl, extraClass = _getSWFCSS(), - s, x, sClass, side = null, isRTL = null, + s, x, sClass, isRTL = null, html = _doc.getElementsByTagName('html')[0]; isRTL = (html && html.dir && html.dir.match(/rtl/i)); smID = (typeof smID === 'undefined'?_s.id:smID); @@ -4451,16 +4840,10 @@ 'wmode': _s.wmode, // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html 'hasPriority': 'true' }; - if (side !== null) { - // don't specify width/height if null. - oEmbed.width = side; - oEmbed.height = side; - } - if (_s.debugFlash) { oEmbed.FlashVars = 'debug=1'; } if (!_s.wmode) { @@ -4471,11 +4854,11 @@ if (_isIE) { // IE is "special". oMovie = _doc.createElement('div'); movieHTML = [ - '<object id="' + smID + '" data="' + smURL + '" type="' + oEmbed.type + '" title="' + oEmbed.title +'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="' + _http+'download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" width="' + oEmbed.width + '" height="' + oEmbed.height + '">', + '<object id="' + smID + '" data="' + smURL + '" type="' + oEmbed.type + '" title="' + oEmbed.title +'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="' + _http+'download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0">', param('movie', smURL), param('AllowScriptAccess', _s.allowScriptAccess), param('quality', oEmbed.quality), (_s.wmode? param('wmode', _s.wmode): ''), param('bgcolor', _s.bgColor), @@ -4627,11 +5010,11 @@ if (_flash) { _wDS('waitEI'); } // </d> - if (_s.oninitmovie instanceof Function) { + if (typeof _s.oninitmovie === 'function') { setTimeout(_s.oninitmovie, 1); } return true; @@ -4643,33 +5026,46 @@ }; _waitForEI = function() { + var p, + loadIncomplete = false; + if (_waitingForEI) { return false; } _waitingForEI = true; _event.remove(_win, 'load', _delayWaitForEI); if (_tryInitOnFocus && !_isFocused) { - // giant Safari 3.1 hack - assume mousemove = focus given lack of focus event + // Safari won't load flash in background tabs, only when focused. _wDS('waitFocus'); return false; } - var p; if (!_didInit) { p = _s.getMoviePercent(); - _s._wD(_str('waitImpatient', (p === 100?' (SWF loaded)':(p > 0?' (SWF ' + p + '% loaded)':'')))); + _s._wD(_str('waitImpatient', (p > 0 ? ' (SWF ' + p + '% loaded)' : ''))); + if (p > 0 && p < 100) { + loadIncomplete = true; + } } setTimeout(function() { - p = _s.getMoviePercent(); + p = _s.getMoviePercent(); + if (loadIncomplete) { + // special case: if movie *partially* loaded, retry until it's 100% before assuming failure. + _waitingForEI = false; + _s._wD(_str('waitSWF')); + _win.setTimeout(_delayWaitForEI, 1); + return false; + } + // <d> if (!_didInit) { _s._wD(_sm + ': No Flash response within expected time.\nLikely causes: ' + (p === 0?'Loading ' + _s.movieURL + ' may have failed (and/or Flash ' + _fV + '+ not present?), ':'') + 'Flash blocked or JS-Flash security error.' + (_s.debugFlash?' ' + _str('checkSWF'):''), 2); if (!_overHTTP && p) { _wDS('localFail', 2); @@ -4715,29 +5111,28 @@ _handleFocus = function() { function cleanup() { _event.remove(_win, 'focus', _handleFocus); - _event.remove(_win, 'load', _handleFocus); } if (_isFocused || !_tryInitOnFocus) { + // already focused, or not special Safari background tab case cleanup(); return true; } _okToDisable = true; _isFocused = true; - _s._wD(_smc+'handleFocus()'); + _s._wD(_sm+': Got window focus.'); - if (_isSafari && _tryInitOnFocus) { - _event.remove(_win, 'mousemove', _handleFocus); - } - // allow init to restart _waitingForEI = false; + // kick off ExternalInterface timeout, now that the SWF has started + _delayWaitForEI(); + cleanup(); return true; }; @@ -4770,10 +5165,11 @@ _debugTS('onload', true); return true; } var wasTimeout = (_s.useFlashBlock && _s.flashLoadTimeout && !_s.getMoviePercent()), + result = true, error; if (!wasTimeout) { _didInit = true; if (_disabled) { @@ -4785,35 +5181,70 @@ if (_disabled || bNoDisable) { if (_s.useFlashBlock && _s.oMC) { _s.oMC.className = _getSWFCSS() + ' ' + (_s.getMoviePercent() === null?_swfCSS.swfTimedout:_swfCSS.swfError); } - _processOnEvents({type:'ontimeout', error:error}); + _processOnEvents({type:'ontimeout', error:error, ignoreInit: true}); _debugTS('onload', false); _catchError(error); - return false; + result = false; } else { _debugTS('onload', true); } - if (_s.waitForWindowLoad && !_windowLoaded) { - _wDS('waitOnload'); - _event.add(_win, 'load', _initUserOnload); - return false; - } else { - // <d> - if (_s.waitForWindowLoad && _windowLoaded) { - _wDS('docLoaded'); + if (!_disabled) { + if (_s.waitForWindowLoad && !_windowLoaded) { + _wDS('waitOnload'); + _event.add(_win, 'load', _initUserOnload); + } else { + // <d> + if (_s.waitForWindowLoad && _windowLoaded) { + _wDS('docLoaded'); + } + // </d> + _initUserOnload(); } - // </d> - _initUserOnload(); } - return true; + return result; }; + /** + * apply top-level setupOptions object as local properties, eg., this.setupOptions.flashVersion -> this.flashVersion (soundManager.flashVersion) + * this maintains backward compatibility, and allows properties to be defined separately for use by soundManager.setup(). + */ + + _setProperties = function() { + + var i, + o = _s.setupOptions; + + for (i in o) { + + if (o.hasOwnProperty(i)) { + + // assign local property if not already defined + + if (typeof _s[i] === 'undefined') { + + _s[i] = o[i]; + + } else if (_s[i] !== o[i]) { + + // legacy support: write manually-assigned property (eg., soundManager.url) back to setupOptions to keep things in sync + _s.setupOptions[i] = _s[i]; + + } + + } + + } + + }; + + _init = function() { _wDS('init'); // called after onload() @@ -4892,47 +5323,61 @@ if (_didDCLoaded) { return false; } _didDCLoaded = true; + + // assign top-level soundManager properties eg. soundManager.url + _setProperties(); + _initDebug(); /** * Temporary feature: allow force of HTML5 via URL params: sm2-usehtml5audio=0 or 1 * Ditto for sm2-preferFlash, too. */ // <d> (function(){ - var a = 'sm2-usehtml5audio=', l = _wl.toLowerCase(), b = null, - a2 = 'sm2-preferflash=', b2 = null, hasCon = (typeof console !== 'undefined' && typeof console.log !== 'undefined'); + var a = 'sm2-usehtml5audio=', + a2 = 'sm2-preferflash=', + b = null, + b2 = null, + hasCon = (typeof console !== 'undefined' && typeof console.log === 'function'), + l = _wl.toLowerCase(); if (l.indexOf(a) !== -1) { b = (l.charAt(l.indexOf(a)+a.length) === '1'); if (hasCon) { console.log((b?'Enabling ':'Disabling ')+'useHTML5Audio via URL parameter'); } - _s.useHTML5Audio = b; + _s.setup({ + 'useHTML5Audio': b + }); } if (l.indexOf(a2) !== -1) { b2 = (l.charAt(l.indexOf(a2)+a2.length) === '1'); if (hasCon) { console.log((b2?'Enabling ':'Disabling ')+'preferFlash via URL parameter'); } - _s.preferFlash = b2; + _s.setup({ + 'preferFlash': b2 + }); } }()); // </d> if (!_hasFlash && _s.hasHTML5) { _s._wD('SoundManager: No Flash detected'+(!_s.useHTML5Audio?', enabling HTML5.':'. Trying HTML5-only mode.')); - _s.useHTML5Audio = true; - // make sure we aren't preferring flash, either - // TODO: preferFlash should not matter if flash is not installed. Currently, stuff breaks without the below tweak. - _s.preferFlash = false; + _s.setup({ + 'useHTML5Audio': true, + // make sure we aren't preferring flash, either + // TODO: preferFlash should not matter if flash is not installed. Currently, stuff breaks without the below tweak. + 'preferFlash': false + }); } _testHTML5(); _s.html5.usingFlash = _featureCheck(); _needsFlash = _s.html5.usingFlash; @@ -4940,11 +5385,13 @@ if (!_hasFlash && _needsFlash) { _s._wD('SoundManager: Fatal error: Flash is needed to play some required formats, but is not available.'); // TODO: Fatal here vs. timeout approach, etc. // hack: fail sooner. - _s.flashLoadTimeout = 1; + _s.setup({ + 'flashLoadTimeout': 1 + }); } if (_doc.removeEventListener) { _doc.removeEventListener('DOMContentLoaded', _domContentLoaded, false); } @@ -4974,20 +5421,13 @@ // sniff up-front _detectFlash(); // focus and window load, init (primarily flash-driven) _event.add(_win, 'focus', _handleFocus); - _event.add(_win, 'load', _handleFocus); _event.add(_win, 'load', _delayWaitForEI); _event.add(_win, 'load', _winOnLoad); - - if (_isSafari && _tryInitOnFocus) { - // massive Safari 3.1 focus detection hack - _event.add(_win, 'mousemove', _handleFocus); - } - if (_doc.addEventListener) { _doc.addEventListener('DOMContentLoaded', _domContentLoaded, false); } else if (_doc.attachEvent) { @@ -5021,6 +5461,6 @@ */ window.SoundManager = SoundManager; // constructor window.soundManager = soundManager; // public API, flash callbacks etc. -}(window)); +}(window)); \ No newline at end of file