LibreJS compatibility modifications
[KiwiIRC.git] / client / assets / libs / soundmanager2 / soundmanager2.js
1 /** @license
2 *
3 * SoundManager 2: JavaScript Sound for the Web
4 * ----------------------------------------------
5 * http://schillmania.com/projects/soundmanager2/
6 *
7 * Copyright (c) 2007, Scott Schiller. All rights reserved.
8 * Code provided under the BSD License:
9 * http://schillmania.com/projects/soundmanager2/license.txt
10 *
11 * V2.97a.20130324 ("Mahalo" Edition)
12 */
13
14 /*global window, SM2_DEFER, sm2Debugger, console, document, navigator, setTimeout, setInterval, clearInterval, Audio, opera */
15 /*jslint regexp: true, sloppy: true, white: true, nomen: true, plusplus: true, todo: true */
16
17 /**
18 * About this file
19 * -------------------------------------------------------------------------------------
20 * This is the fully-commented source version of the SoundManager 2 API,
21 * recommended for use during development and testing.
22 *
23 * See soundmanager2-nodebug-jsmin.js for an optimized build (~11KB with gzip.)
24 * http://schillmania.com/projects/soundmanager2/doc/getstarted/#basic-inclusion
25 * Alternately, serve this file with gzip for 75% compression savings (~30KB over HTTP.)
26 *
27 * You may notice <d> and </d> comments in this source; these are delimiters for
28 * debug blocks which are removed in the -nodebug builds, further optimizing code size.
29 *
30 * Also, as you may note: Whoa, reliable cross-platform/device audio support is hard! ;)
31 */
32
33 (function(window, _undefined) {
34
35 "use strict";
36
37 var soundManager = null;
38
39 /**
40 * The SoundManager constructor.
41 *
42 * @constructor
43 * @param {string} smURL Optional: Path to SWF files
44 * @param {string} smID Optional: The ID to use for the SWF container element
45 * @this {SoundManager}
46 * @return {SoundManager} The new SoundManager instance
47 */
48
49 function SoundManager(smURL, smID) {
50
51 /**
52 * soundManager configuration options list
53 * defines top-level configuration properties to be applied to the soundManager instance (eg. soundManager.flashVersion)
54 * to set these properties, use the setup() method - eg., soundManager.setup({url: '/swf/', flashVersion: 9})
55 */
56
57 this.setupOptions = {
58
59 'url': (smURL || null), // path (directory) where SoundManager 2 SWFs exist, eg., /path/to/swfs/
60 'flashVersion': 8, // flash build to use (8 or 9.) Some API features require 9.
61 'debugMode': true, // enable debugging output (console.log() with HTML fallback)
62 'debugFlash': false, // enable debugging output inside SWF, troubleshoot Flash/browser issues
63 'useConsole': true, // use console.log() if available (otherwise, writes to #soundmanager-debug element)
64 'consoleOnly': true, // if console is being used, do not create/write to #soundmanager-debug
65 'waitForWindowLoad': false, // force SM2 to wait for window.onload() before trying to call soundManager.onload()
66 'bgColor': '#ffffff', // SWF background color. N/A when wmode = 'transparent'
67 'useHighPerformance': false, // position:fixed flash movie can help increase js/flash speed, minimize lag
68 'flashPollingInterval': null, // msec affecting whileplaying/loading callback frequency. If null, default of 50 msec is used.
69 'html5PollingInterval': null, // msec affecting whileplaying() for HTML5 audio, excluding mobile devices. If null, native HTML5 update events are used.
70 'flashLoadTimeout': 1000, // msec to wait for flash movie to load before failing (0 = infinity)
71 'wmode': null, // flash rendering mode - null, 'transparent', or 'opaque' (last two allow z-index to work)
72 'allowScriptAccess': 'always', // for scripting the SWF (object/embed property), 'always' or 'sameDomain'
73 'useFlashBlock': false, // *requires flashblock.css, see demos* - allow recovery from flash blockers. Wait indefinitely and apply timeout CSS to SWF, if applicable.
74 'useHTML5Audio': true, // use HTML5 Audio() where API is supported (most Safari, Chrome versions), Firefox (no MP3/MP4.) Ideally, transparent vs. Flash API where possible.
75 'html5Test': /^(probably|maybe)$/i, // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative.
76 '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.
77 'noSWFCache': false // if true, appends ?ts={date} to break aggressive SWF caching.
78
79 };
80
81 this.defaultOptions = {
82
83 /**
84 * the default configuration for sound objects made with createSound() and related methods
85 * eg., volume, auto-load behaviour and so forth
86 */
87
88 '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)
89 'autoPlay': false, // enable playing of file as soon as possible (much faster if "stream" is true)
90 'from': null, // position to start playback within a sound (msec), default = beginning
91 'loops': 1, // how many times to repeat the sound (position will wrap around to 0, setPosition() will break out of loop when >0)
92 'onid3': null, // callback function for "ID3 data is added/available"
93 'onload': null, // callback function for "load finished"
94 'whileloading': null, // callback function for "download progress update" (X of Y bytes received)
95 'onplay': null, // callback for "play" start
96 'onpause': null, // callback for "pause"
97 'onresume': null, // callback for "resume" (pause toggle)
98 'whileplaying': null, // callback during play (position update)
99 'onposition': null, // object containing times and function callbacks for positions of interest
100 'onstop': null, // callback for "user stop"
101 'onfailure': null, // callback function for when playing fails
102 'onfinish': null, // callback function for "sound finished playing"
103 'multiShot': true, // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
104 'multiShotEvents': false, // fire multiple sound events (currently onfinish() only) when multiShot is enabled
105 'position': null, // offset (milliseconds) to seek to within loaded sound data.
106 'pan': 0, // "pan" settings, left-to-right, -100 to 100
107 'stream': true, // allows playing before entire file has loaded (recommended)
108 'to': null, // position to end playback within a sound (msec), default = end
109 'type': null, // MIME-like hint for file pattern / canPlay() tests, eg. audio/mp3
110 'usePolicyFile': false, // enable crossdomain.xml request for audio on remote domains (for ID3/waveform access)
111 'volume': 100 // self-explanatory. 0-100, the latter being the max.
112
113 };
114
115 this.flash9Options = {
116
117 /**
118 * flash 9-only options,
119 * merged into defaultOptions if flash 9 is being used
120 */
121
122 'isMovieStar': null, // "MovieStar" MPEG4 audio mode. Null (default) = auto detect MP4, AAC etc. based on URL. true = force on, ignore URL
123 'usePeakData': false, // enable left/right channel peak (level) data
124 'useWaveformData': false, // enable sound spectrum (raw waveform data) - NOTE: May increase CPU load.
125 'useEQData': false, // enable sound EQ (frequency spectrum data) - NOTE: May increase CPU load.
126 'onbufferchange': null, // callback for "isBuffering" property change
127 'ondataerror': null // callback for waveform/eq data access error (flash playing audio in other tabs/domains)
128
129 };
130
131 this.movieStarOptions = {
132
133 /**
134 * flash 9.0r115+ MPEG4 audio options,
135 * merged into defaultOptions if flash 9+movieStar mode is enabled
136 */
137
138 'bufferTime': 3, // seconds of data to buffer before playback begins (null = flash default of 0.1 seconds - if AAC playback is gappy, try increasing.)
139 'serverURL': null, // rtmp: FMS or FMIS server to connect to, required when requesting media via RTMP or one of its variants
140 'onconnect': null, // rtmp: callback for connection to flash media server
141 'duration': null // rtmp: song duration (msec)
142
143 };
144
145 this.audioFormats = {
146
147 /**
148 * determines HTML5 support + flash requirements.
149 * if no support (via flash and/or HTML5) for a "required" format, SM2 will fail to start.
150 * flash fallback is used for MP3 or MP4 if HTML5 can't play it (or if preferFlash = true)
151 */
152
153 'mp3': {
154 'type': ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'],
155 'required': true
156 },
157
158 'mp4': {
159 'related': ['aac','m4a','m4b'], // additional formats under the MP4 container
160 'type': ['audio/mp4; codecs="mp4a.40.2"', 'audio/aac', 'audio/x-m4a', 'audio/MP4A-LATM', 'audio/mpeg4-generic'],
161 'required': false
162 },
163
164 'ogg': {
165 'type': ['audio/ogg; codecs=vorbis'],
166 'required': false
167 },
168
169 'opus': {
170 'type': ['audio/ogg; codecs=opus', 'audio/opus'],
171 'required': false
172 },
173
174 'wav': {
175 'type': ['audio/wav; codecs="1"', 'audio/wav', 'audio/wave', 'audio/x-wav'],
176 'required': false
177 }
178
179 };
180
181 // HTML attributes (id + class names) for the SWF container
182
183 this.movieID = 'sm2-container';
184 this.id = (smID || 'sm2movie');
185
186 this.debugID = 'soundmanager-debug';
187 this.debugURLParam = /([#?&])debug=1/i;
188
189 // dynamic attributes
190
191 this.versionNumber = 'V2.97a.20130324';
192 this.version = null;
193 this.movieURL = null;
194 this.altURL = null;
195 this.swfLoaded = false;
196 this.enabled = false;
197 this.oMC = null;
198 this.sounds = {};
199 this.soundIDs = [];
200 this.muted = false;
201 this.didFlashBlock = false;
202 this.filePattern = null;
203
204 this.filePatterns = {
205
206 'flash8': /\.mp3(\?.*)?$/i,
207 'flash9': /\.mp3(\?.*)?$/i
208
209 };
210
211 // support indicators, set at init
212
213 this.features = {
214
215 'buffering': false,
216 'peakData': false,
217 'waveformData': false,
218 'eqData': false,
219 'movieStar': false
220
221 };
222
223 // flash sandbox info, used primarily in troubleshooting
224
225 this.sandbox = {
226
227 // <d>
228 'type': null,
229 'types': {
230 'remote': 'remote (domain-based) rules',
231 'localWithFile': 'local with file access (no internet access)',
232 'localWithNetwork': 'local with network (internet access only, no local access)',
233 'localTrusted': 'local, trusted (local+internet access)'
234 },
235 'description': null,
236 'noRemote': null,
237 'noLocal': null
238 // </d>
239
240 };
241
242 /**
243 * format support (html5/flash)
244 * stores canPlayType() results based on audioFormats.
245 * eg. { mp3: boolean, mp4: boolean }
246 * treat as read-only.
247 */
248
249 this.html5 = {
250 'usingFlash': null // set if/when flash fallback is needed
251 };
252
253 // file type support hash
254 this.flash = {};
255
256 // determined at init time
257 this.html5Only = false;
258
259 // used for special cases (eg. iPad/iPhone/palm OS?)
260 this.ignoreFlash = false;
261
262 /**
263 * a few private internals (OK, a lot. :D)
264 */
265
266 var SMSound,
267 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, setVersionInfo, handleFocus, strings, initMovie, preInit, 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 = [],
268 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,
269 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)),
270 mobileHTML5 = (ua.match(/(mobile|pre\/|xoom)/i) || is_iDevice || isAndroid),
271 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
272 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,
273 emptyURL = 'about:blank', // safe URL to unload, or load nothing from (flash 8 + most HTML5 UAs)
274 overHTTP = (doc.location?doc.location.protocol.match(/http/i):null),
275 http = (!overHTTP ? 'http:/'+'/' : ''),
276 // mp3, mp4, aac etc.
277 netStreamMimeTypes = /^\s*audio\/(?:x-)?(?:mpeg4|aac|flv|mov|mp4||m4v|m4a|m4b|mp4v|3gp|3g2)\s*(?:$|;)/i,
278 // Flash v9.0r115+ "moviestar" formats
279 netStreamTypes = ['mpeg4', 'aac', 'flv', 'mov', 'mp4', 'm4v', 'f4v', 'm4a', 'm4b', 'mp4v', '3gp', '3g2'],
280 netStreamPattern = new RegExp('\\.(' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i');
281
282 this.mimePattern = /^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i; // default mp3 set
283
284 // use altURL if not "online"
285 this.useAltURL = !overHTTP;
286
287 swfCSS = {
288
289 'swfBox': 'sm2-object-box',
290 'swfDefault': 'movieContainer',
291 'swfError': 'swf_error', // SWF loaded, but SM2 couldn't start (other error)
292 'swfTimedout': 'swf_timedout',
293 'swfLoaded': 'swf_loaded',
294 'swfUnblocked': 'swf_unblocked', // or loaded OK
295 'sm2Debug': 'sm2_debug',
296 'highPerf': 'high_performance',
297 'flashDebug': 'flash_debug'
298
299 };
300
301 /**
302 * basic HTML5 Audio() support test
303 * try...catch because of IE 9 "not implemented" nonsense
304 * https://github.com/Modernizr/Modernizr/issues/224
305 */
306
307 this.hasHTML5 = (function() {
308 try {
309 // new Audio(null) for stupid Opera 9.64 case, which throws not_enough_arguments exception otherwise.
310 return (Audio !== _undefined && (isOpera && opera !== _undefined && opera.version() < 10 ? new Audio(null) : new Audio()).canPlayType !== _undefined);
311 } catch(e) {
312 return false;
313 }
314 }());
315
316 /**
317 * Public SoundManager API
318 * -----------------------
319 */
320
321 /**
322 * Configures top-level soundManager properties.
323 *
324 * @param {object} options Option parameters, eg. { flashVersion: 9, url: '/path/to/swfs/' }
325 * onready and ontimeout are also accepted parameters. call soundManager.setup() to see the full list.
326 */
327
328 this.setup = function(options) {
329
330 var noURL = (!sm2.url);
331
332 // warn if flash options have already been applied
333
334 if (options !== _undefined && didInit && needsFlash && sm2.ok() && (options.flashVersion !== _undefined || options.url !== _undefined || options.html5Test !== _undefined)) {
335 complain(str('setupLate'));
336 }
337
338 // TODO: defer: true?
339
340 assign(options);
341
342 // 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.
343
344 if (noURL && didDCLoaded && options.url !== _undefined) {
345 sm2.beginDelayedInit();
346 }
347
348 // special case 2: If lazy-loading SM2 (DOMContentLoaded has already happened) and user calls setup() with url: parameter, try to init ASAP.
349
350 if (!didDCLoaded && options.url !== _undefined && doc.readyState === 'complete') {
351 setTimeout(domContentLoaded, 1);
352 }
353
354 return sm2;
355
356 };
357
358 this.ok = function() {
359
360 return (needsFlash?(didInit && !disabled):(sm2.useHTML5Audio && sm2.hasHTML5));
361
362 };
363
364 this.supported = this.ok; // legacy
365
366 this.getMovie = function(smID) {
367
368 // safety net: some old browsers differ on SWF references, possibly related to ExternalInterface / flash version
369 return id(smID) || doc[smID] || window[smID];
370
371 };
372
373 /**
374 * Creates a SMSound sound object instance.
375 *
376 * @param {object} oOptions Sound options (at minimum, id and url parameters are required.)
377 * @return {object} SMSound The new SMSound object.
378 */
379
380 this.createSound = function(oOptions, _url) {
381
382 var cs, cs_string, options, oSound = null;
383
384 // <d>
385 cs = sm + '.createSound(): ';
386 cs_string = cs + str(!didInit?'notReady':'notOK');
387 // </d>
388
389 if (!didInit || !sm2.ok()) {
390 complain(cs_string);
391 return false;
392 }
393
394 if (_url !== _undefined) {
395 // function overloading in JS! :) ..assume simple createSound(id,url) use case
396 oOptions = {
397 'id': oOptions,
398 'url': _url
399 };
400 }
401
402 // inherit from defaultOptions
403 options = mixin(oOptions);
404
405 options.url = parseURL(options.url);
406
407 // <d>
408 if (options.id.toString().charAt(0).match(/^[0-9]$/)) {
409 sm2._wD(cs + str('badID', options.id), 2);
410 }
411
412 sm2._wD(cs + options.id + ' (' + options.url + ')', 1);
413 // </d>
414
415 if (idCheck(options.id, true)) {
416 sm2._wD(cs + options.id + ' exists', 1);
417 return sm2.sounds[options.id];
418 }
419
420 function make() {
421
422 options = loopFix(options);
423 sm2.sounds[options.id] = new SMSound(options);
424 sm2.soundIDs.push(options.id);
425 return sm2.sounds[options.id];
426
427 }
428
429 if (html5OK(options)) {
430
431 oSound = make();
432 sm2._wD(options.id + ': Using HTML5');
433 oSound._setup_html5(options);
434
435 } else {
436
437 if (fV > 8) {
438 if (options.isMovieStar === null) {
439 // attempt to detect MPEG-4 formats
440 options.isMovieStar = !!(options.serverURL || (options.type ? options.type.match(netStreamMimeTypes) : false) || options.url.match(netStreamPattern));
441 }
442 // <d>
443 if (options.isMovieStar) {
444 sm2._wD(cs + 'using MovieStar handling');
445 if (options.loops > 1) {
446 _wDS('noNSLoop');
447 }
448 }
449 // </d>
450 }
451
452 options = policyFix(options, cs);
453 oSound = make();
454
455 if (fV === 8) {
456 flash._createSound(options.id, options.loops||1, options.usePolicyFile);
457 } else {
458 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);
459 if (!options.serverURL) {
460 // We are connected immediately
461 oSound.connected = true;
462 if (options.onconnect) {
463 options.onconnect.apply(oSound);
464 }
465 }
466 }
467
468 if (!options.serverURL && (options.autoLoad || options.autoPlay)) {
469 // call load for non-rtmp streams
470 oSound.load(options);
471 }
472
473 }
474
475 // rtmp will play in onconnect
476 if (!options.serverURL && options.autoPlay) {
477 oSound.play();
478 }
479
480 return oSound;
481
482 };
483
484 /**
485 * Destroys a SMSound sound object instance.
486 *
487 * @param {string} sID The ID of the sound to destroy
488 */
489
490 this.destroySound = function(sID, _bFromSound) {
491
492 // explicitly destroy a sound before normal page unload, etc.
493
494 if (!idCheck(sID)) {
495 return false;
496 }
497
498 var oS = sm2.sounds[sID], i;
499
500 // Disable all callbacks while the sound is being destroyed
501 oS._iO = {};
502
503 oS.stop();
504 oS.unload();
505
506 for (i = 0; i < sm2.soundIDs.length; i++) {
507 if (sm2.soundIDs[i] === sID) {
508 sm2.soundIDs.splice(i, 1);
509 break;
510 }
511 }
512
513 if (!_bFromSound) {
514 // ignore if being called from SMSound instance
515 oS.destruct(true);
516 }
517
518 oS = null;
519 delete sm2.sounds[sID];
520
521 return true;
522
523 };
524
525 /**
526 * Calls the load() method of a SMSound object by ID.
527 *
528 * @param {string} sID The ID of the sound
529 * @param {object} oOptions Optional: Sound options
530 */
531
532 this.load = function(sID, oOptions) {
533
534 if (!idCheck(sID)) {
535 return false;
536 }
537 return sm2.sounds[sID].load(oOptions);
538
539 };
540
541 /**
542 * Calls the unload() method of a SMSound object by ID.
543 *
544 * @param {string} sID The ID of the sound
545 */
546
547 this.unload = function(sID) {
548
549 if (!idCheck(sID)) {
550 return false;
551 }
552 return sm2.sounds[sID].unload();
553
554 };
555
556 /**
557 * Calls the onPosition() method of a SMSound object by ID.
558 *
559 * @param {string} sID The ID of the sound
560 * @param {number} nPosition The position to watch for
561 * @param {function} oMethod The relevant callback to fire
562 * @param {object} oScope Optional: The scope to apply the callback to
563 * @return {SMSound} The SMSound object
564 */
565
566 this.onPosition = function(sID, nPosition, oMethod, oScope) {
567
568 if (!idCheck(sID)) {
569 return false;
570 }
571 return sm2.sounds[sID].onposition(nPosition, oMethod, oScope);
572
573 };
574
575 // legacy/backwards-compability: lower-case method name
576 this.onposition = this.onPosition;
577
578 /**
579 * Calls the clearOnPosition() method of a SMSound object by ID.
580 *
581 * @param {string} sID The ID of the sound
582 * @param {number} nPosition The position to watch for
583 * @param {function} oMethod Optional: The relevant callback to fire
584 * @return {SMSound} The SMSound object
585 */
586
587 this.clearOnPosition = function(sID, nPosition, oMethod) {
588
589 if (!idCheck(sID)) {
590 return false;
591 }
592 return sm2.sounds[sID].clearOnPosition(nPosition, oMethod);
593
594 };
595
596 /**
597 * Calls the play() method of a SMSound object by ID.
598 *
599 * @param {string} sID The ID of the sound
600 * @param {object} oOptions Optional: Sound options
601 * @return {SMSound} The SMSound object
602 */
603
604 this.play = function(sID, oOptions) {
605
606 var result = false;
607
608 if (!didInit || !sm2.ok()) {
609 complain(sm + '.play(): ' + str(!didInit?'notReady':'notOK'));
610 return result;
611 }
612
613 if (!idCheck(sID)) {
614 if (!(oOptions instanceof Object)) {
615 // overloading use case: play('mySound','/path/to/some.mp3');
616 oOptions = {
617 url: oOptions
618 };
619 }
620 if (oOptions && oOptions.url) {
621 // overloading use case, create+play: .play('someID',{url:'/path/to.mp3'});
622 sm2._wD(sm + '.play(): attempting to create "' + sID + '"', 1);
623 oOptions.id = sID;
624 result = sm2.createSound(oOptions).play();
625 }
626 return result;
627 }
628
629 return sm2.sounds[sID].play(oOptions);
630
631 };
632
633 this.start = this.play; // just for convenience
634
635 /**
636 * Calls the setPosition() method of a SMSound object by ID.
637 *
638 * @param {string} sID The ID of the sound
639 * @param {number} nMsecOffset Position (milliseconds)
640 * @return {SMSound} The SMSound object
641 */
642
643 this.setPosition = function(sID, nMsecOffset) {
644
645 if (!idCheck(sID)) {
646 return false;
647 }
648 return sm2.sounds[sID].setPosition(nMsecOffset);
649
650 };
651
652 /**
653 * Calls the stop() method of a SMSound object by ID.
654 *
655 * @param {string} sID The ID of the sound
656 * @return {SMSound} The SMSound object
657 */
658
659 this.stop = function(sID) {
660
661 if (!idCheck(sID)) {
662 return false;
663 }
664
665 sm2._wD(sm + '.stop(' + sID + ')', 1);
666 return sm2.sounds[sID].stop();
667
668 };
669
670 /**
671 * Stops all currently-playing sounds.
672 */
673
674 this.stopAll = function() {
675
676 var oSound;
677 sm2._wD(sm + '.stopAll()', 1);
678
679 for (oSound in sm2.sounds) {
680 if (sm2.sounds.hasOwnProperty(oSound)) {
681 // apply only to sound objects
682 sm2.sounds[oSound].stop();
683 }
684 }
685
686 };
687
688 /**
689 * Calls the pause() method of a SMSound object by ID.
690 *
691 * @param {string} sID The ID of the sound
692 * @return {SMSound} The SMSound object
693 */
694
695 this.pause = function(sID) {
696
697 if (!idCheck(sID)) {
698 return false;
699 }
700 return sm2.sounds[sID].pause();
701
702 };
703
704 /**
705 * Pauses all currently-playing sounds.
706 */
707
708 this.pauseAll = function() {
709
710 var i;
711 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
712 sm2.sounds[sm2.soundIDs[i]].pause();
713 }
714
715 };
716
717 /**
718 * Calls the resume() method of a SMSound object by ID.
719 *
720 * @param {string} sID The ID of the sound
721 * @return {SMSound} The SMSound object
722 */
723
724 this.resume = function(sID) {
725
726 if (!idCheck(sID)) {
727 return false;
728 }
729 return sm2.sounds[sID].resume();
730
731 };
732
733 /**
734 * Resumes all currently-paused sounds.
735 */
736
737 this.resumeAll = function() {
738
739 var i;
740 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
741 sm2.sounds[sm2.soundIDs[i]].resume();
742 }
743
744 };
745
746 /**
747 * Calls the togglePause() method of a SMSound object by ID.
748 *
749 * @param {string} sID The ID of the sound
750 * @return {SMSound} The SMSound object
751 */
752
753 this.togglePause = function(sID) {
754
755 if (!idCheck(sID)) {
756 return false;
757 }
758 return sm2.sounds[sID].togglePause();
759
760 };
761
762 /**
763 * Calls the setPan() method of a SMSound object by ID.
764 *
765 * @param {string} sID The ID of the sound
766 * @param {number} nPan The pan value (-100 to 100)
767 * @return {SMSound} The SMSound object
768 */
769
770 this.setPan = function(sID, nPan) {
771
772 if (!idCheck(sID)) {
773 return false;
774 }
775 return sm2.sounds[sID].setPan(nPan);
776
777 };
778
779 /**
780 * Calls the setVolume() method of a SMSound object by ID.
781 *
782 * @param {string} sID The ID of the sound
783 * @param {number} nVol The volume value (0 to 100)
784 * @return {SMSound} The SMSound object
785 */
786
787 this.setVolume = function(sID, nVol) {
788
789 if (!idCheck(sID)) {
790 return false;
791 }
792 return sm2.sounds[sID].setVolume(nVol);
793
794 };
795
796 /**
797 * Calls the mute() method of either a single SMSound object by ID, or all sound objects.
798 *
799 * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
800 */
801
802 this.mute = function(sID) {
803
804 var i = 0;
805
806 if (sID instanceof String) {
807 sID = null;
808 }
809
810 if (!sID) {
811
812 sm2._wD(sm + '.mute(): Muting all sounds');
813 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
814 sm2.sounds[sm2.soundIDs[i]].mute();
815 }
816 sm2.muted = true;
817
818 } else {
819
820 if (!idCheck(sID)) {
821 return false;
822 }
823 sm2._wD(sm + '.mute(): Muting "' + sID + '"');
824 return sm2.sounds[sID].mute();
825
826 }
827
828 return true;
829
830 };
831
832 /**
833 * Mutes all sounds.
834 */
835
836 this.muteAll = function() {
837
838 sm2.mute();
839
840 };
841
842 /**
843 * Calls the unmute() method of either a single SMSound object by ID, or all sound objects.
844 *
845 * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
846 */
847
848 this.unmute = function(sID) {
849
850 var i;
851
852 if (sID instanceof String) {
853 sID = null;
854 }
855
856 if (!sID) {
857
858 sm2._wD(sm + '.unmute(): Unmuting all sounds');
859 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
860 sm2.sounds[sm2.soundIDs[i]].unmute();
861 }
862 sm2.muted = false;
863
864 } else {
865
866 if (!idCheck(sID)) {
867 return false;
868 }
869 sm2._wD(sm + '.unmute(): Unmuting "' + sID + '"');
870 return sm2.sounds[sID].unmute();
871
872 }
873
874 return true;
875
876 };
877
878 /**
879 * Unmutes all sounds.
880 */
881
882 this.unmuteAll = function() {
883
884 sm2.unmute();
885
886 };
887
888 /**
889 * Calls the toggleMute() method of a SMSound object by ID.
890 *
891 * @param {string} sID The ID of the sound
892 * @return {SMSound} The SMSound object
893 */
894
895 this.toggleMute = function(sID) {
896
897 if (!idCheck(sID)) {
898 return false;
899 }
900 return sm2.sounds[sID].toggleMute();
901
902 };
903
904 /**
905 * Retrieves the memory used by the flash plugin.
906 *
907 * @return {number} The amount of memory in use
908 */
909
910 this.getMemoryUse = function() {
911
912 // flash-only
913 var ram = 0;
914
915 if (flash && fV !== 8) {
916 ram = parseInt(flash._getMemoryUse(), 10);
917 }
918
919 return ram;
920
921 };
922
923 /**
924 * Undocumented: NOPs soundManager and all SMSound objects.
925 */
926
927 this.disable = function(bNoDisable) {
928
929 // destroy all functions
930 var i;
931
932 if (bNoDisable === _undefined) {
933 bNoDisable = false;
934 }
935
936 if (disabled) {
937 return false;
938 }
939
940 disabled = true;
941 _wDS('shutdown', 1);
942
943 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
944 disableObject(sm2.sounds[sm2.soundIDs[i]]);
945 }
946
947 // fire "complete", despite fail
948 initComplete(bNoDisable);
949 event.remove(window, 'load', initUserOnload);
950
951 return true;
952
953 };
954
955 /**
956 * Determines playability of a MIME type, eg. 'audio/mp3'.
957 */
958
959 this.canPlayMIME = function(sMIME) {
960
961 var result;
962
963 if (sm2.hasHTML5) {
964 result = html5CanPlay({type:sMIME});
965 }
966
967 if (!result && needsFlash) {
968 // if flash 9, test netStream (movieStar) types as well.
969 result = (sMIME && sm2.ok() ? !!((fV > 8 ? sMIME.match(netStreamMimeTypes) : null) || sMIME.match(sm2.mimePattern)) : null);
970 }
971
972 return result;
973
974 };
975
976 /**
977 * Determines playability of a URL based on audio support.
978 *
979 * @param {string} sURL The URL to test
980 * @return {boolean} URL playability
981 */
982
983 this.canPlayURL = function(sURL) {
984
985 var result;
986
987 if (sm2.hasHTML5) {
988 result = html5CanPlay({url: sURL});
989 }
990
991 if (!result && needsFlash) {
992 result = (sURL && sm2.ok() ? !!(sURL.match(sm2.filePattern)) : null);
993 }
994
995 return result;
996
997 };
998
999 /**
1000 * Determines playability of an HTML DOM &lt;a&gt; object (or similar object literal) based on audio support.
1001 *
1002 * @param {object} oLink an HTML DOM &lt;a&gt; object or object literal including href and/or type attributes
1003 * @return {boolean} URL playability
1004 */
1005
1006 this.canPlayLink = function(oLink) {
1007
1008 if (oLink.type !== _undefined && oLink.type) {
1009 if (sm2.canPlayMIME(oLink.type)) {
1010 return true;
1011 }
1012 }
1013
1014 return sm2.canPlayURL(oLink.href);
1015
1016 };
1017
1018 /**
1019 * Retrieves a SMSound object by ID.
1020 *
1021 * @param {string} sID The ID of the sound
1022 * @return {SMSound} The SMSound object
1023 */
1024
1025 this.getSoundById = function(sID, _suppressDebug) {
1026
1027 if (!sID) {
1028 throw new Error(sm + '.getSoundById(): sID is null/_undefined');
1029 }
1030
1031 var result = sm2.sounds[sID];
1032
1033 // <d>
1034 if (!result && !_suppressDebug) {
1035 sm2._wD('"' + sID + '" is an invalid sound ID.', 2);
1036 }
1037 // </d>
1038
1039 return result;
1040
1041 };
1042
1043 /**
1044 * Queues a callback for execution when SoundManager has successfully initialized.
1045 *
1046 * @param {function} oMethod The callback method to fire
1047 * @param {object} oScope Optional: The scope to apply to the callback
1048 */
1049
1050 this.onready = function(oMethod, oScope) {
1051
1052 var sType = 'onready',
1053 result = false;
1054
1055 if (typeof oMethod === 'function') {
1056
1057 // <d>
1058 if (didInit) {
1059 sm2._wD(str('queue', sType));
1060 }
1061 // </d>
1062
1063 if (!oScope) {
1064 oScope = window;
1065 }
1066
1067 addOnEvent(sType, oMethod, oScope);
1068 processOnEvents();
1069
1070 result = true;
1071
1072 } else {
1073
1074 throw str('needFunction', sType);
1075
1076 }
1077
1078 return result;
1079
1080 };
1081
1082 /**
1083 * Queues a callback for execution when SoundManager has failed to initialize.
1084 *
1085 * @param {function} oMethod The callback method to fire
1086 * @param {object} oScope Optional: The scope to apply to the callback
1087 */
1088
1089 this.ontimeout = function(oMethod, oScope) {
1090
1091 var sType = 'ontimeout',
1092 result = false;
1093
1094 if (typeof oMethod === 'function') {
1095
1096 // <d>
1097 if (didInit) {
1098 sm2._wD(str('queue', sType));
1099 }
1100 // </d>
1101
1102 if (!oScope) {
1103 oScope = window;
1104 }
1105
1106 addOnEvent(sType, oMethod, oScope);
1107 processOnEvents({type:sType});
1108
1109 result = true;
1110
1111 } else {
1112
1113 throw str('needFunction', sType);
1114
1115 }
1116
1117 return result;
1118
1119 };
1120
1121 /**
1122 * Writes console.log()-style debug output to a console or in-browser element.
1123 * Applies when debugMode = true
1124 *
1125 * @param {string} sText The console message
1126 * @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.
1127 */
1128
1129 this._writeDebug = function(sText, sTypeOrObject) {
1130
1131 // pseudo-private console.log()-style output
1132 // <d>
1133
1134 var sDID = 'soundmanager-debug', o, oItem;
1135
1136 if (!sm2.debugMode) {
1137 return false;
1138 }
1139
1140 if (hasConsole && sm2.useConsole) {
1141 if (sTypeOrObject && typeof sTypeOrObject === 'object') {
1142 // object passed; dump to console.
1143 console.log(sText, sTypeOrObject);
1144 } else if (debugLevels[sTypeOrObject] !== _undefined) {
1145 console[debugLevels[sTypeOrObject]](sText);
1146 } else {
1147 console.log(sText);
1148 }
1149 if (sm2.consoleOnly) {
1150 return true;
1151 }
1152 }
1153
1154 o = id(sDID);
1155
1156 if (!o) {
1157 return false;
1158 }
1159
1160 oItem = doc.createElement('div');
1161
1162 if (++wdCount % 2 === 0) {
1163 oItem.className = 'sm2-alt';
1164 }
1165
1166 if (sTypeOrObject === _undefined) {
1167 sTypeOrObject = 0;
1168 } else {
1169 sTypeOrObject = parseInt(sTypeOrObject, 10);
1170 }
1171
1172 oItem.appendChild(doc.createTextNode(sText));
1173
1174 if (sTypeOrObject) {
1175 if (sTypeOrObject >= 2) {
1176 oItem.style.fontWeight = 'bold';
1177 }
1178 if (sTypeOrObject === 3) {
1179 oItem.style.color = '#ff3333';
1180 }
1181 }
1182
1183 // top-to-bottom
1184 // o.appendChild(oItem);
1185
1186 // bottom-to-top
1187 o.insertBefore(oItem, o.firstChild);
1188
1189 o = null;
1190 // </d>
1191
1192 return true;
1193
1194 };
1195
1196 // <d>
1197 // last-resort debugging option
1198 if (wl.indexOf('sm2-debug=alert') !== -1) {
1199 this._writeDebug = function(sText) {
1200 window.alert(sText);
1201 };
1202 }
1203 // </d>
1204
1205 // alias
1206 this._wD = this._writeDebug;
1207
1208 /**
1209 * Provides debug / state information on all SMSound objects.
1210 */
1211
1212 this._debug = function() {
1213
1214 // <d>
1215 var i, j;
1216 _wDS('currentObj', 1);
1217
1218 for (i = 0, j = sm2.soundIDs.length; i < j; i++) {
1219 sm2.sounds[sm2.soundIDs[i]]._debug();
1220 }
1221 // </d>
1222
1223 };
1224
1225 /**
1226 * Restarts and re-initializes the SoundManager instance.
1227 *
1228 * @param {boolean} resetEvents Optional: When true, removes all registered onready and ontimeout event callbacks.
1229 * @param {boolean} excludeInit Options: When true, does not call beginDelayedInit() (which would restart SM2).
1230 * @return {object} soundManager The soundManager instance.
1231 */
1232
1233 this.reboot = function(resetEvents, excludeInit) {
1234
1235 // reset some (or all) state, and re-init unless otherwise specified.
1236
1237 // <d>
1238 if (sm2.soundIDs.length) {
1239 sm2._wD('Destroying ' + sm2.soundIDs.length + ' SMSound objects...');
1240 }
1241 // </d>
1242
1243 var i, j, k;
1244
1245 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
1246 sm2.sounds[sm2.soundIDs[i]].destruct();
1247 }
1248
1249 // trash ze flash
1250
1251 if (flash) {
1252
1253 try {
1254
1255 if (isIE) {
1256 oRemovedHTML = flash.innerHTML;
1257 }
1258
1259 oRemoved = flash.parentNode.removeChild(flash);
1260
1261 _wDS('flRemoved');
1262
1263 } catch(e) {
1264
1265 // Remove failed? May be due to flash blockers silently removing the SWF object/embed node from the DOM. Warn and continue.
1266
1267 _wDS('badRemove', 2);
1268
1269 }
1270
1271 }
1272
1273 // actually, force recreate of movie.
1274
1275 oRemovedHTML = oRemoved = needsFlash = flash = null;
1276
1277 sm2.enabled = didDCLoaded = didInit = waitingForEI = initPending = didAppend = appendSuccess = disabled = useGlobalHTML5Audio = sm2.swfLoaded = false;
1278
1279 sm2.soundIDs = [];
1280 sm2.sounds = {};
1281
1282 if (!resetEvents) {
1283 // reset callbacks for onready, ontimeout etc. so that they will fire again on re-init
1284 for (i in on_queue) {
1285 if (on_queue.hasOwnProperty(i)) {
1286 for (j = 0, k = on_queue[i].length; j < k; j++) {
1287 on_queue[i][j].fired = false;
1288 }
1289 }
1290 }
1291 } else {
1292 // remove all callbacks entirely
1293 on_queue = [];
1294 }
1295
1296 // <d>
1297 if (!excludeInit) {
1298 sm2._wD(sm + ': Rebooting...');
1299 }
1300 // </d>
1301
1302 // reset HTML5 and flash canPlay test results
1303
1304 sm2.html5 = {
1305 'usingFlash': null
1306 };
1307
1308 sm2.flash = {};
1309
1310 // reset device-specific HTML/flash mode switches
1311
1312 sm2.html5Only = false;
1313 sm2.ignoreFlash = false;
1314
1315 window.setTimeout(function() {
1316
1317 preInit();
1318
1319 // by default, re-init
1320
1321 if (!excludeInit) {
1322 sm2.beginDelayedInit();
1323 }
1324
1325 }, 20);
1326
1327 return sm2;
1328
1329 };
1330
1331 this.reset = function() {
1332
1333 /**
1334 * Shuts down and restores the SoundManager instance to its original loaded state, without an explicit reboot. All onready/ontimeout handlers are removed.
1335 * After this call, SM2 may be re-initialized via soundManager.beginDelayedInit().
1336 * @return {object} soundManager The soundManager instance.
1337 */
1338
1339 _wDS('reset');
1340 return sm2.reboot(true, true);
1341
1342 };
1343
1344 /**
1345 * Undocumented: Determines the SM2 flash movie's load progress.
1346 *
1347 * @return {number or null} Percent loaded, or if invalid/unsupported, null.
1348 */
1349
1350 this.getMoviePercent = function() {
1351
1352 /**
1353 * Interesting syntax notes...
1354 * Flash/ExternalInterface (ActiveX/NPAPI) bridge methods are not typeof "function" nor instanceof Function, but are still valid.
1355 * Additionally, JSLint dislikes ('PercentLoaded' in flash)-style syntax and recommends hasOwnProperty(), which does not work in this case.
1356 * Furthermore, using (flash && flash.PercentLoaded) causes IE to throw "object doesn't support this property or method".
1357 * Thus, 'in' syntax must be used.
1358 */
1359
1360 return (flash && 'PercentLoaded' in flash ? flash.PercentLoaded() : null); // Yes, JSLint. See nearby comment in source for explanation.
1361
1362 };
1363
1364 /**
1365 * Additional helper for manually invoking SM2's init process after DOM Ready / window.onload().
1366 */
1367
1368 this.beginDelayedInit = function() {
1369
1370 windowLoaded = true;
1371 domContentLoaded();
1372
1373 setTimeout(function() {
1374
1375 if (initPending) {
1376 return false;
1377 }
1378
1379 createMovie();
1380 initMovie();
1381 initPending = true;
1382
1383 return true;
1384
1385 }, 20);
1386
1387 delayWaitForEI();
1388
1389 };
1390
1391 /**
1392 * Destroys the SoundManager instance and all SMSound instances.
1393 */
1394
1395 this.destruct = function() {
1396
1397 sm2._wD(sm + '.destruct()');
1398 sm2.disable(true);
1399
1400 };
1401
1402 /**
1403 * SMSound() (sound object) constructor
1404 * ------------------------------------
1405 *
1406 * @param {object} oOptions Sound options (id and url are required attributes)
1407 * @return {SMSound} The new SMSound object
1408 */
1409
1410 SMSound = function(oOptions) {
1411
1412 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;
1413
1414 lastHTML5State = {
1415 // tracks duration + position (time)
1416 duration: null,
1417 time: null
1418 };
1419
1420 this.id = oOptions.id;
1421
1422 // legacy
1423 this.sID = this.id;
1424
1425 this.url = oOptions.url;
1426 this.options = mixin(oOptions);
1427
1428 // per-play-instance-specific options
1429 this.instanceOptions = this.options;
1430
1431 // short alias
1432 this._iO = this.instanceOptions;
1433
1434 // assign property defaults
1435 this.pan = this.options.pan;
1436 this.volume = this.options.volume;
1437
1438 // whether or not this object is using HTML5
1439 this.isHTML5 = false;
1440
1441 // internal HTML5 Audio() object reference
1442 this._a = null;
1443
1444 /**
1445 * SMSound() public methods
1446 * ------------------------
1447 */
1448
1449 this.id3 = {};
1450
1451 /**
1452 * Writes SMSound object parameters to debug console
1453 */
1454
1455 this._debug = function() {
1456
1457 // <d>
1458 sm2._wD(s.id + ': Merged options:', s.options);
1459 // </d>
1460
1461 };
1462
1463 /**
1464 * Begins loading a sound per its *url*.
1465 *
1466 * @param {object} oOptions Optional: Sound options
1467 * @return {SMSound} The SMSound object
1468 */
1469
1470 this.load = function(oOptions) {
1471
1472 var oSound = null, instanceOptions;
1473
1474 if (oOptions !== _undefined) {
1475 s._iO = mixin(oOptions, s.options);
1476 } else {
1477 oOptions = s.options;
1478 s._iO = oOptions;
1479 if (lastURL && lastURL !== s.url) {
1480 _wDS('manURL');
1481 s._iO.url = s.url;
1482 s.url = null;
1483 }
1484 }
1485
1486 if (!s._iO.url) {
1487 s._iO.url = s.url;
1488 }
1489
1490 s._iO.url = parseURL(s._iO.url);
1491
1492 // ensure we're in sync
1493 s.instanceOptions = s._iO;
1494
1495 // local shortcut
1496 instanceOptions = s._iO;
1497
1498 sm2._wD(s.id + ': load (' + instanceOptions.url + ')');
1499
1500 if (instanceOptions.url === s.url && s.readyState !== 0 && s.readyState !== 2) {
1501 _wDS('onURL', 1);
1502 // if loaded and an onload() exists, fire immediately.
1503 if (s.readyState === 3 && instanceOptions.onload) {
1504 // assume success based on truthy duration.
1505 wrapCallback(s, function() {
1506 instanceOptions.onload.apply(s, [(!!s.duration)]);
1507 });
1508 }
1509 return s;
1510 }
1511
1512 // reset a few state properties
1513
1514 s.loaded = false;
1515 s.readyState = 1;
1516 s.playState = 0;
1517 s.id3 = {};
1518
1519 // TODO: If switching from HTML5 -> flash (or vice versa), stop currently-playing audio.
1520
1521 if (html5OK(instanceOptions)) {
1522
1523 oSound = s._setup_html5(instanceOptions);
1524
1525 if (!oSound._called_load) {
1526
1527 s._html5_canplay = false;
1528
1529 // TODO: review called_load / html5_canplay logic
1530
1531 // if url provided directly to load(), assign it here.
1532
1533 if (s.url !== instanceOptions.url) {
1534
1535 sm2._wD(_wDS('manURL') + ': ' + instanceOptions.url);
1536
1537 s._a.src = instanceOptions.url;
1538
1539 // TODO: review / re-apply all relevant options (volume, loop, onposition etc.)
1540
1541 // reset position for new URL
1542 s.setPosition(0);
1543
1544 }
1545
1546 // given explicit load call, try to preload.
1547
1548 // early HTML5 implementation (non-standard)
1549 s._a.autobuffer = 'auto';
1550
1551 // standard
1552 s._a.preload = 'auto';
1553
1554 s._a._called_load = true;
1555
1556 if (instanceOptions.autoPlay) {
1557 s.play();
1558 }
1559
1560 } else {
1561
1562 sm2._wD(s.id + ': Ignoring request to load again');
1563
1564 }
1565
1566 } else {
1567
1568 try {
1569 s.isHTML5 = false;
1570 s._iO = policyFix(loopFix(instanceOptions));
1571 // re-assign local shortcut
1572 instanceOptions = s._iO;
1573 if (fV === 8) {
1574 flash._load(s.id, instanceOptions.url, instanceOptions.stream, instanceOptions.autoPlay, instanceOptions.usePolicyFile);
1575 } else {
1576 flash._load(s.id, instanceOptions.url, !!(instanceOptions.stream), !!(instanceOptions.autoPlay), instanceOptions.loops||1, !!(instanceOptions.autoLoad), instanceOptions.usePolicyFile);
1577 }
1578 } catch(e) {
1579 _wDS('smError', 2);
1580 debugTS('onload', false);
1581 catchError({type:'SMSOUND_LOAD_JS_EXCEPTION', fatal:true});
1582 }
1583
1584 }
1585
1586 // after all of this, ensure sound url is up to date.
1587 s.url = instanceOptions.url;
1588
1589 return s;
1590
1591 };
1592
1593 /**
1594 * Unloads a sound, canceling any open HTTP requests.
1595 *
1596 * @return {SMSound} The SMSound object
1597 */
1598
1599 this.unload = function() {
1600
1601 // Flash 8/AS2 can't "close" a stream - fake it by loading an empty URL
1602 // Flash 9/AS3: Close stream, preventing further load
1603 // HTML5: Most UAs will use empty URL
1604
1605 if (s.readyState !== 0) {
1606
1607 sm2._wD(s.id + ': unload()');
1608
1609 if (!s.isHTML5) {
1610
1611 if (fV === 8) {
1612 flash._unload(s.id, emptyURL);
1613 } else {
1614 flash._unload(s.id);
1615 }
1616
1617 } else {
1618
1619 stop_html5_timer();
1620
1621 if (s._a) {
1622
1623 s._a.pause();
1624 html5Unload(s._a, emptyURL);
1625
1626 // update empty URL, too
1627 lastURL = emptyURL;
1628
1629 }
1630
1631 }
1632
1633 // reset load/status flags
1634 resetProperties();
1635
1636 }
1637
1638 return s;
1639
1640 };
1641
1642 /**
1643 * Unloads and destroys a sound.
1644 */
1645
1646 this.destruct = function(_bFromSM) {
1647
1648 sm2._wD(s.id + ': Destruct');
1649
1650 if (!s.isHTML5) {
1651
1652 // kill sound within Flash
1653 // Disable the onfailure handler
1654 s._iO.onfailure = null;
1655 flash._destroySound(s.id);
1656
1657 } else {
1658
1659 stop_html5_timer();
1660
1661 if (s._a) {
1662 s._a.pause();
1663 html5Unload(s._a);
1664 if (!useGlobalHTML5Audio) {
1665 remove_html5_events();
1666 }
1667 // break obvious circular reference
1668 s._a._s = null;
1669 s._a = null;
1670 }
1671
1672 }
1673
1674 if (!_bFromSM) {
1675 // ensure deletion from controller
1676 sm2.destroySound(s.id, true);
1677
1678 }
1679
1680 };
1681
1682 /**
1683 * Begins playing a sound.
1684 *
1685 * @param {object} oOptions Optional: Sound options
1686 * @return {SMSound} The SMSound object
1687 */
1688
1689 this.play = function(oOptions, _updatePlayState) {
1690
1691 var fN, allowMulti, a, onready, startOK = true,
1692 exit = null;
1693
1694 // <d>
1695 fN = s.id + ': play(): ';
1696 // </d>
1697
1698 // default to true
1699 _updatePlayState = (_updatePlayState === _undefined ? true : _updatePlayState);
1700
1701 if (!oOptions) {
1702 oOptions = {};
1703 }
1704
1705 // first, use local URL (if specified)
1706 if (s.url) {
1707 s._iO.url = s.url;
1708 }
1709
1710 // mix in any options defined at createSound()
1711 s._iO = mixin(s._iO, s.options);
1712
1713 // mix in any options specific to this method
1714 s._iO = mixin(oOptions, s._iO);
1715
1716 s._iO.url = parseURL(s._iO.url);
1717
1718 s.instanceOptions = s._iO;
1719
1720 // RTMP-only
1721 if (s._iO.serverURL && !s.connected) {
1722 if (!s.getAutoPlay()) {
1723 sm2._wD(fN +' Netstream not connected yet - setting autoPlay');
1724 s.setAutoPlay(true);
1725 }
1726 // play will be called in onconnect()
1727 return s;
1728 }
1729
1730 if (html5OK(s._iO)) {
1731 s._setup_html5(s._iO);
1732 start_html5_timer();
1733 }
1734
1735 if (s.playState === 1 && !s.paused) {
1736 allowMulti = s._iO.multiShot;
1737 if (!allowMulti) {
1738 sm2._wD(fN + 'Already playing (one-shot)', 1);
1739 exit = s;
1740 } else {
1741 sm2._wD(fN + 'Already playing (multi-shot)', 1);
1742 }
1743 }
1744
1745 if (exit !== null) {
1746 return exit;
1747 }
1748
1749 // edge case: play() with explicit URL parameter
1750 if (oOptions.url && oOptions.url !== s.url) {
1751 // load using merged options
1752 s.load(s._iO);
1753 }
1754
1755 if (!s.loaded) {
1756
1757 if (s.readyState === 0) {
1758
1759 sm2._wD(fN + 'Attempting to load');
1760
1761 // try to get this sound playing ASAP
1762 if (!s.isHTML5) {
1763 // assign directly because setAutoPlay() increments the instanceCount
1764 s._iO.autoPlay = true;
1765 s.load(s._iO);
1766 } else {
1767 // iOS needs this when recycling sounds, loading a new URL on an existing object.
1768 s.load(s._iO);
1769 }
1770
1771 // HTML5 hack - re-set instanceOptions?
1772 s.instanceOptions = s._iO;
1773
1774 } else if (s.readyState === 2) {
1775
1776 sm2._wD(fN + 'Could not load - exiting', 2);
1777 exit = s;
1778
1779 } else {
1780
1781 sm2._wD(fN + 'Loading - attempting to play...');
1782
1783 }
1784
1785 } else {
1786
1787 // "play()"
1788 sm2._wD(fN.substr(0, fN.lastIndexOf(':')));
1789
1790 }
1791
1792 if (exit !== null) {
1793 return exit;
1794 }
1795
1796 if (!s.isHTML5 && fV === 9 && s.position > 0 && s.position === s.duration) {
1797 // flash 9 needs a position reset if play() is called while at the end of a sound.
1798 sm2._wD(fN + 'Sound at end, resetting to position:0');
1799 oOptions.position = 0;
1800 }
1801
1802 /**
1803 * Streams will pause when their buffer is full if they are being loaded.
1804 * In this case paused is true, but the song hasn't started playing yet.
1805 * If we just call resume() the onplay() callback will never be called.
1806 * So only call resume() if the position is > 0.
1807 * Another reason is because options like volume won't have been applied yet.
1808 * For normal sounds, just resume.
1809 */
1810
1811 if (s.paused && s.position >= 0 && (!s._iO.serverURL || s.position > 0)) {
1812
1813 // https://gist.github.com/37b17df75cc4d7a90bf6
1814 sm2._wD(fN + 'Resuming from paused state', 1);
1815 s.resume();
1816
1817 } else {
1818
1819 s._iO = mixin(oOptions, s._iO);
1820
1821 // apply from/to parameters, if they exist (and not using RTMP)
1822 if (s._iO.from !== null && s._iO.to !== null && s.instanceCount === 0 && s.playState === 0 && !s._iO.serverURL) {
1823
1824 onready = function() {
1825 // sound "canplay" or onload()
1826 // re-apply from/to to instance options, and start playback
1827 s._iO = mixin(oOptions, s._iO);
1828 s.play(s._iO);
1829 };
1830
1831 // HTML5 needs to at least have "canplay" fired before seeking.
1832 if (s.isHTML5 && !s._html5_canplay) {
1833
1834 // this hasn't been loaded yet. load it first, and then do this again.
1835 sm2._wD(fN + 'Beginning load for from/to case');
1836
1837 s.load({
1838 // TODO: was _oncanplay. Sounds wrong.
1839 oncanplay: onready
1840 });
1841
1842 exit = false;
1843
1844 } else if (!s.isHTML5 && !s.loaded && (!s.readyState || s.readyState !== 2)) {
1845
1846 // to be safe, preload the whole thing in Flash.
1847
1848 sm2._wD(fN + 'Preloading for from/to case');
1849
1850 s.load({
1851 onload: onready
1852 });
1853
1854 exit = false;
1855
1856 }
1857
1858 if (exit !== null) {
1859 return exit;
1860 }
1861
1862 // otherwise, we're ready to go. re-apply local options, and continue
1863
1864 s._iO = applyFromTo();
1865
1866 }
1867
1868 sm2._wD(fN + 'Starting to play');
1869
1870 if (!s.instanceCount || s._iO.multiShotEvents || (!s.isHTML5 && fV > 8 && !s.getAutoPlay())) {
1871 s.instanceCount++;
1872 }
1873
1874 // if first play and onposition parameters exist, apply them now
1875 if (s._iO.onposition && s.playState === 0) {
1876 attachOnPosition(s);
1877 }
1878
1879 s.playState = 1;
1880 s.paused = false;
1881
1882 s.position = (s._iO.position !== _undefined && !isNaN(s._iO.position) ? s._iO.position : 0);
1883
1884 if (!s.isHTML5) {
1885 s._iO = policyFix(loopFix(s._iO));
1886 }
1887
1888 if (s._iO.onplay && _updatePlayState) {
1889 s._iO.onplay.apply(s);
1890 onplay_called = true;
1891 }
1892
1893 s.setVolume(s._iO.volume, true);
1894 s.setPan(s._iO.pan, true);
1895
1896 if (!s.isHTML5) {
1897
1898 startOK = flash._start(s.id, s._iO.loops || 1, (fV === 9 ? s.position : s.position / 1000), s._iO.multiShot || false);
1899
1900 if (fV === 9 && !startOK) {
1901 // edge case: no sound hardware, or 32-channel flash ceiling hit.
1902 // applies only to Flash 9, non-NetStream/MovieStar sounds.
1903 // http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/Sound.html#play%28%29
1904 sm2._wD(fN + 'No sound hardware, or 32-sound ceiling hit');
1905 if (s._iO.onplayerror) {
1906 s._iO.onplayerror.apply(s);
1907 }
1908
1909 }
1910
1911 } else {
1912
1913 start_html5_timer();
1914
1915 a = s._setup_html5();
1916
1917 s.setPosition(s._iO.position);
1918
1919 a.play();
1920
1921 }
1922
1923 }
1924
1925 return s;
1926
1927 };
1928
1929 // just for convenience
1930 this.start = this.play;
1931
1932 /**
1933 * Stops playing a sound (and optionally, all sounds)
1934 *
1935 * @param {boolean} bAll Optional: Whether to stop all sounds
1936 * @return {SMSound} The SMSound object
1937 */
1938
1939 this.stop = function(bAll) {
1940
1941 var instanceOptions = s._iO,
1942 originalPosition;
1943
1944 if (s.playState === 1) {
1945
1946 sm2._wD(s.id + ': stop()');
1947
1948 s._onbufferchange(0);
1949 s._resetOnPosition(0);
1950 s.paused = false;
1951
1952 if (!s.isHTML5) {
1953 s.playState = 0;
1954 }
1955
1956 // remove onPosition listeners, if any
1957 detachOnPosition();
1958
1959 // and "to" position, if set
1960 if (instanceOptions.to) {
1961 s.clearOnPosition(instanceOptions.to);
1962 }
1963
1964 if (!s.isHTML5) {
1965
1966 flash._stop(s.id, bAll);
1967
1968 // hack for netStream: just unload
1969 if (instanceOptions.serverURL) {
1970 s.unload();
1971 }
1972
1973 } else {
1974
1975 if (s._a) {
1976
1977 originalPosition = s.position;
1978
1979 // act like Flash, though
1980 s.setPosition(0);
1981
1982 // hack: reflect old position for onstop() (also like Flash)
1983 s.position = originalPosition;
1984
1985 // html5 has no stop()
1986 // NOTE: pausing means iOS requires interaction to resume.
1987 s._a.pause();
1988
1989 s.playState = 0;
1990
1991 // and update UI
1992 s._onTimer();
1993
1994 stop_html5_timer();
1995
1996 }
1997
1998 }
1999
2000 s.instanceCount = 0;
2001 s._iO = {};
2002
2003 if (instanceOptions.onstop) {
2004 instanceOptions.onstop.apply(s);
2005 }
2006
2007 }
2008
2009 return s;
2010
2011 };
2012
2013 /**
2014 * Undocumented/internal: Sets autoPlay for RTMP.
2015 *
2016 * @param {boolean} autoPlay state
2017 */
2018
2019 this.setAutoPlay = function(autoPlay) {
2020
2021 sm2._wD(s.id + ': Autoplay turned ' + (autoPlay ? 'on' : 'off'));
2022 s._iO.autoPlay = autoPlay;
2023
2024 if (!s.isHTML5) {
2025 flash._setAutoPlay(s.id, autoPlay);
2026 if (autoPlay) {
2027 // only increment the instanceCount if the sound isn't loaded (TODO: verify RTMP)
2028 if (!s.instanceCount && s.readyState === 1) {
2029 s.instanceCount++;
2030 sm2._wD(s.id + ': Incremented instance count to '+s.instanceCount);
2031 }
2032 }
2033 }
2034
2035 };
2036
2037 /**
2038 * Undocumented/internal: Returns the autoPlay boolean.
2039 *
2040 * @return {boolean} The current autoPlay value
2041 */
2042
2043 this.getAutoPlay = function() {
2044
2045 return s._iO.autoPlay;
2046
2047 };
2048
2049 /**
2050 * Sets the position of a sound.
2051 *
2052 * @param {number} nMsecOffset Position (milliseconds)
2053 * @return {SMSound} The SMSound object
2054 */
2055
2056 this.setPosition = function(nMsecOffset) {
2057
2058 if (nMsecOffset === _undefined) {
2059 nMsecOffset = 0;
2060 }
2061
2062 var original_pos,
2063 position, position1K,
2064 // Use the duration from the instance options, if we don't have a track duration yet.
2065 // position >= 0 and <= current available (loaded) duration
2066 offset = (s.isHTML5 ? Math.max(nMsecOffset, 0) : Math.min(s.duration || s._iO.duration, Math.max(nMsecOffset, 0)));
2067
2068 original_pos = s.position;
2069 s.position = offset;
2070 position1K = s.position/1000;
2071 s._resetOnPosition(s.position);
2072 s._iO.position = offset;
2073
2074 if (!s.isHTML5) {
2075
2076 position = (fV === 9 ? s.position : position1K);
2077 if (s.readyState && s.readyState !== 2) {
2078 // if paused or not playing, will not resume (by playing)
2079 flash._setPosition(s.id, position, (s.paused || !s.playState), s._iO.multiShot);
2080 }
2081
2082 } else if (s._a) {
2083
2084 // Set the position in the canplay handler if the sound is not ready yet
2085 if (s._html5_canplay) {
2086 if (s._a.currentTime !== position1K) {
2087 /**
2088 * DOM/JS errors/exceptions to watch out for:
2089 * if seek is beyond (loaded?) position, "DOM exception 11"
2090 * "INDEX_SIZE_ERR": DOM exception 1
2091 */
2092 sm2._wD(s.id + ': setPosition('+position1K+')');
2093 try {
2094 s._a.currentTime = position1K;
2095 if (s.playState === 0 || s.paused) {
2096 // allow seek without auto-play/resume
2097 s._a.pause();
2098 }
2099 } catch(e) {
2100 sm2._wD(s.id + ': setPosition(' + position1K + ') failed: ' + e.message, 2);
2101 }
2102 }
2103 } else {
2104 sm2._wD(s.id + ': setPosition(' + position1K + '): Cannot seek yet, sound not ready');
2105 }
2106
2107 }
2108
2109 if (s.isHTML5) {
2110 if (s.paused) {
2111 // if paused, refresh UI right away
2112 // force update
2113 s._onTimer(true);
2114 }
2115 }
2116
2117 return s;
2118
2119 };
2120
2121 /**
2122 * Pauses sound playback.
2123 *
2124 * @return {SMSound} The SMSound object
2125 */
2126
2127 this.pause = function(_bCallFlash) {
2128
2129 if (s.paused || (s.playState === 0 && s.readyState !== 1)) {
2130 return s;
2131 }
2132
2133 sm2._wD(s.id + ': pause()');
2134 s.paused = true;
2135
2136 if (!s.isHTML5) {
2137 if (_bCallFlash || _bCallFlash === _undefined) {
2138 flash._pause(s.id, s._iO.multiShot);
2139 }
2140 } else {
2141 s._setup_html5().pause();
2142 stop_html5_timer();
2143 }
2144
2145 if (s._iO.onpause) {
2146 s._iO.onpause.apply(s);
2147 }
2148
2149 return s;
2150
2151 };
2152
2153 /**
2154 * Resumes sound playback.
2155 *
2156 * @return {SMSound} The SMSound object
2157 */
2158
2159 /**
2160 * When auto-loaded streams pause on buffer full they have a playState of 0.
2161 * We need to make sure that the playState is set to 1 when these streams "resume".
2162 * When a paused stream is resumed, we need to trigger the onplay() callback if it
2163 * hasn't been called already. In this case since the sound is being played for the
2164 * first time, I think it's more appropriate to call onplay() rather than onresume().
2165 */
2166
2167 this.resume = function() {
2168
2169 var instanceOptions = s._iO;
2170
2171 if (!s.paused) {
2172 return s;
2173 }
2174
2175 sm2._wD(s.id + ': resume()');
2176 s.paused = false;
2177 s.playState = 1;
2178
2179 if (!s.isHTML5) {
2180 if (instanceOptions.isMovieStar && !instanceOptions.serverURL) {
2181 // Bizarre Webkit bug (Chrome reported via 8tracks.com dudes): AAC content paused for 30+ seconds(?) will not resume without a reposition.
2182 s.setPosition(s.position);
2183 }
2184 // flash method is toggle-based (pause/resume)
2185 flash._pause(s.id, instanceOptions.multiShot);
2186 } else {
2187 s._setup_html5().play();
2188 start_html5_timer();
2189 }
2190
2191 if (!onplay_called && instanceOptions.onplay) {
2192 instanceOptions.onplay.apply(s);
2193 onplay_called = true;
2194 } else if (instanceOptions.onresume) {
2195 instanceOptions.onresume.apply(s);
2196 }
2197
2198 return s;
2199
2200 };
2201
2202 /**
2203 * Toggles sound playback.
2204 *
2205 * @return {SMSound} The SMSound object
2206 */
2207
2208 this.togglePause = function() {
2209
2210 sm2._wD(s.id + ': togglePause()');
2211
2212 if (s.playState === 0) {
2213 s.play({
2214 position: (fV === 9 && !s.isHTML5 ? s.position : s.position / 1000)
2215 });
2216 return s;
2217 }
2218
2219 if (s.paused) {
2220 s.resume();
2221 } else {
2222 s.pause();
2223 }
2224
2225 return s;
2226
2227 };
2228
2229 /**
2230 * Sets the panning (L-R) effect.
2231 *
2232 * @param {number} nPan The pan value (-100 to 100)
2233 * @return {SMSound} The SMSound object
2234 */
2235
2236 this.setPan = function(nPan, bInstanceOnly) {
2237
2238 if (nPan === _undefined) {
2239 nPan = 0;
2240 }
2241
2242 if (bInstanceOnly === _undefined) {
2243 bInstanceOnly = false;
2244 }
2245
2246 if (!s.isHTML5) {
2247 flash._setPan(s.id, nPan);
2248 } // else { no HTML5 pan? }
2249
2250 s._iO.pan = nPan;
2251
2252 if (!bInstanceOnly) {
2253 s.pan = nPan;
2254 s.options.pan = nPan;
2255 }
2256
2257 return s;
2258
2259 };
2260
2261 /**
2262 * Sets the volume.
2263 *
2264 * @param {number} nVol The volume value (0 to 100)
2265 * @return {SMSound} The SMSound object
2266 */
2267
2268 this.setVolume = function(nVol, _bInstanceOnly) {
2269
2270 /**
2271 * Note: Setting volume has no effect on iOS "special snowflake" devices.
2272 * Hardware volume control overrides software, and volume
2273 * will always return 1 per Apple docs. (iOS 4 + 5.)
2274 * http://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingSoundtoCanvasAnimations/AddingSoundtoCanvasAnimations.html
2275 */
2276
2277 if (nVol === _undefined) {
2278 nVol = 100;
2279 }
2280
2281 if (_bInstanceOnly === _undefined) {
2282 _bInstanceOnly = false;
2283 }
2284
2285 if (!s.isHTML5) {
2286 flash._setVolume(s.id, (sm2.muted && !s.muted) || s.muted?0:nVol);
2287 } else if (s._a) {
2288 // valid range: 0-1
2289 s._a.volume = Math.max(0, Math.min(1, nVol/100));
2290 }
2291
2292 s._iO.volume = nVol;
2293
2294 if (!_bInstanceOnly) {
2295 s.volume = nVol;
2296 s.options.volume = nVol;
2297 }
2298
2299 return s;
2300
2301 };
2302
2303 /**
2304 * Mutes the sound.
2305 *
2306 * @return {SMSound} The SMSound object
2307 */
2308
2309 this.mute = function() {
2310
2311 s.muted = true;
2312
2313 if (!s.isHTML5) {
2314 flash._setVolume(s.id, 0);
2315 } else if (s._a) {
2316 s._a.muted = true;
2317 }
2318
2319 return s;
2320
2321 };
2322
2323 /**
2324 * Unmutes the sound.
2325 *
2326 * @return {SMSound} The SMSound object
2327 */
2328
2329 this.unmute = function() {
2330
2331 s.muted = false;
2332 var hasIO = (s._iO.volume !== _undefined);
2333
2334 if (!s.isHTML5) {
2335 flash._setVolume(s.id, hasIO?s._iO.volume:s.options.volume);
2336 } else if (s._a) {
2337 s._a.muted = false;
2338 }
2339
2340 return s;
2341
2342 };
2343
2344 /**
2345 * Toggles the muted state of a sound.
2346 *
2347 * @return {SMSound} The SMSound object
2348 */
2349
2350 this.toggleMute = function() {
2351
2352 return (s.muted?s.unmute():s.mute());
2353
2354 };
2355
2356 /**
2357 * Registers a callback to be fired when a sound reaches a given position during playback.
2358 *
2359 * @param {number} nPosition The position to watch for
2360 * @param {function} oMethod The relevant callback to fire
2361 * @param {object} oScope Optional: The scope to apply the callback to
2362 * @return {SMSound} The SMSound object
2363 */
2364
2365 this.onPosition = function(nPosition, oMethod, oScope) {
2366
2367 // TODO: basic dupe checking?
2368
2369 onPositionItems.push({
2370 position: parseInt(nPosition, 10),
2371 method: oMethod,
2372 scope: (oScope !== _undefined ? oScope : s),
2373 fired: false
2374 });
2375
2376 return s;
2377
2378 };
2379
2380 // legacy/backwards-compability: lower-case method name
2381 this.onposition = this.onPosition;
2382
2383 /**
2384 * Removes registered callback(s) from a sound, by position and/or callback.
2385 *
2386 * @param {number} nPosition The position to clear callback(s) for
2387 * @param {function} oMethod Optional: Identify one callback to be removed when multiple listeners exist for one position
2388 * @return {SMSound} The SMSound object
2389 */
2390
2391 this.clearOnPosition = function(nPosition, oMethod) {
2392
2393 var i;
2394
2395 nPosition = parseInt(nPosition, 10);
2396
2397 if (isNaN(nPosition)) {
2398 // safety check
2399 return false;
2400 }
2401
2402 for (i=0; i < onPositionItems.length; i++) {
2403
2404 if (nPosition === onPositionItems[i].position) {
2405 // remove this item if no method was specified, or, if the method matches
2406 if (!oMethod || (oMethod === onPositionItems[i].method)) {
2407 if (onPositionItems[i].fired) {
2408 // decrement "fired" counter, too
2409 onPositionFired--;
2410 }
2411 onPositionItems.splice(i, 1);
2412 }
2413 }
2414
2415 }
2416
2417 };
2418
2419 this._processOnPosition = function() {
2420
2421 var i, item, j = onPositionItems.length;
2422
2423 if (!j || !s.playState || onPositionFired >= j) {
2424 return false;
2425 }
2426
2427 for (i=j-1; i >= 0; i--) {
2428 item = onPositionItems[i];
2429 if (!item.fired && s.position >= item.position) {
2430 item.fired = true;
2431 onPositionFired++;
2432 item.method.apply(item.scope, [item.position]);
2433 }
2434 }
2435
2436 return true;
2437
2438 };
2439
2440 this._resetOnPosition = function(nPosition) {
2441
2442 // reset "fired" for items interested in this position
2443 var i, item, j = onPositionItems.length;
2444
2445 if (!j) {
2446 return false;
2447 }
2448
2449 for (i=j-1; i >= 0; i--) {
2450 item = onPositionItems[i];
2451 if (item.fired && nPosition <= item.position) {
2452 item.fired = false;
2453 onPositionFired--;
2454 }
2455 }
2456
2457 return true;
2458
2459 };
2460
2461 /**
2462 * SMSound() private internals
2463 * --------------------------------
2464 */
2465
2466 applyFromTo = function() {
2467
2468 var instanceOptions = s._iO,
2469 f = instanceOptions.from,
2470 t = instanceOptions.to,
2471 start, end;
2472
2473 end = function() {
2474
2475 // end has been reached.
2476 sm2._wD(s.id + ': "To" time of ' + t + ' reached.');
2477
2478 // detach listener
2479 s.clearOnPosition(t, end);
2480
2481 // stop should clear this, too
2482 s.stop();
2483
2484 };
2485
2486 start = function() {
2487
2488 sm2._wD(s.id + ': Playing "from" ' + f);
2489
2490 // add listener for end
2491 if (t !== null && !isNaN(t)) {
2492 s.onPosition(t, end);
2493 }
2494
2495 };
2496
2497 if (f !== null && !isNaN(f)) {
2498
2499 // apply to instance options, guaranteeing correct start position.
2500 instanceOptions.position = f;
2501
2502 // multiShot timing can't be tracked, so prevent that.
2503 instanceOptions.multiShot = false;
2504
2505 start();
2506
2507 }
2508
2509 // return updated instanceOptions including starting position
2510 return instanceOptions;
2511
2512 };
2513
2514 attachOnPosition = function() {
2515
2516 var item,
2517 op = s._iO.onposition;
2518
2519 // attach onposition things, if any, now.
2520
2521 if (op) {
2522
2523 for (item in op) {
2524 if (op.hasOwnProperty(item)) {
2525 s.onPosition(parseInt(item, 10), op[item]);
2526 }
2527 }
2528
2529 }
2530
2531 };
2532
2533 detachOnPosition = function() {
2534
2535 var item,
2536 op = s._iO.onposition;
2537
2538 // detach any onposition()-style listeners.
2539
2540 if (op) {
2541
2542 for (item in op) {
2543 if (op.hasOwnProperty(item)) {
2544 s.clearOnPosition(parseInt(item, 10));
2545 }
2546 }
2547
2548 }
2549
2550 };
2551
2552 start_html5_timer = function() {
2553
2554 if (s.isHTML5) {
2555 startTimer(s);
2556 }
2557
2558 };
2559
2560 stop_html5_timer = function() {
2561
2562 if (s.isHTML5) {
2563 stopTimer(s);
2564 }
2565
2566 };
2567
2568 resetProperties = function(retainPosition) {
2569
2570 if (!retainPosition) {
2571 onPositionItems = [];
2572 onPositionFired = 0;
2573 }
2574
2575 onplay_called = false;
2576
2577 s._hasTimer = null;
2578 s._a = null;
2579 s._html5_canplay = false;
2580 s.bytesLoaded = null;
2581 s.bytesTotal = null;
2582 s.duration = (s._iO && s._iO.duration ? s._iO.duration : null);
2583 s.durationEstimate = null;
2584 s.buffered = [];
2585
2586 // legacy: 1D array
2587 s.eqData = [];
2588
2589 s.eqData.left = [];
2590 s.eqData.right = [];
2591
2592 s.failures = 0;
2593 s.isBuffering = false;
2594 s.instanceOptions = {};
2595 s.instanceCount = 0;
2596 s.loaded = false;
2597 s.metadata = {};
2598
2599 // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
2600 s.readyState = 0;
2601
2602 s.muted = false;
2603 s.paused = false;
2604
2605 s.peakData = {
2606 left: 0,
2607 right: 0
2608 };
2609
2610 s.waveformData = {
2611 left: [],
2612 right: []
2613 };
2614
2615 s.playState = 0;
2616 s.position = null;
2617
2618 s.id3 = {};
2619
2620 };
2621
2622 resetProperties();
2623
2624 /**
2625 * Pseudo-private SMSound internals
2626 * --------------------------------
2627 */
2628
2629 this._onTimer = function(bForce) {
2630
2631 /**
2632 * HTML5-only _whileplaying() etc.
2633 * called from both HTML5 native events, and polling/interval-based timers
2634 * mimics flash and fires only when time/duration change, so as to be polling-friendly
2635 */
2636
2637 var duration, isNew = false, time, x = {};
2638
2639 if (s._hasTimer || bForce) {
2640
2641 // TODO: May not need to track readyState (1 = loading)
2642
2643 if (s._a && (bForce || ((s.playState > 0 || s.readyState === 1) && !s.paused))) {
2644
2645 duration = s._get_html5_duration();
2646
2647 if (duration !== lastHTML5State.duration) {
2648
2649 lastHTML5State.duration = duration;
2650 s.duration = duration;
2651 isNew = true;
2652
2653 }
2654
2655 // TODO: investigate why this goes wack if not set/re-set each time.
2656 s.durationEstimate = s.duration;
2657
2658 time = (s._a.currentTime * 1000 || 0);
2659
2660 if (time !== lastHTML5State.time) {
2661
2662 lastHTML5State.time = time;
2663 isNew = true;
2664
2665 }
2666
2667 if (isNew || bForce) {
2668
2669 s._whileplaying(time,x,x,x,x);
2670
2671 }
2672
2673 }/* else {
2674
2675 // sm2._wD('_onTimer: Warn for "'+s.id+'": '+(!s._a?'Could not find element. ':'')+(s.playState === 0?'playState bad, 0?':'playState = '+s.playState+', OK'));
2676
2677 return false;
2678
2679 }*/
2680
2681 return isNew;
2682
2683 }
2684
2685 };
2686
2687 this._get_html5_duration = function() {
2688
2689 var instanceOptions = s._iO,
2690 // if audio object exists, use its duration - else, instance option duration (if provided - it's a hack, really, and should be retired) OR null
2691 d = (s._a && s._a.duration ? s._a.duration*1000 : (instanceOptions && instanceOptions.duration ? instanceOptions.duration : null)),
2692 result = (d && !isNaN(d) && d !== Infinity ? d : null);
2693
2694 return result;
2695
2696 };
2697
2698 this._apply_loop = function(a, nLoops) {
2699
2700 /**
2701 * boolean instead of "loop", for webkit? - spec says string. http://www.w3.org/TR/html-markup/audio.html#audio.attrs.loop
2702 * note that loop is either off or infinite under HTML5, unlike Flash which allows arbitrary loop counts to be specified.
2703 */
2704
2705 // <d>
2706 if (!a.loop && nLoops > 1) {
2707 sm2._wD('Note: Native HTML5 looping is infinite.', 1);
2708 }
2709 // </d>
2710
2711 a.loop = (nLoops > 1 ? 'loop' : '');
2712
2713 };
2714
2715 this._setup_html5 = function(oOptions) {
2716
2717 var instanceOptions = mixin(s._iO, oOptions),
2718 a = useGlobalHTML5Audio ? globalHTML5Audio : s._a,
2719 dURL = decodeURI(instanceOptions.url),
2720 sameURL;
2721
2722 /**
2723 * "First things first, I, Poppa..." (reset the previous state of the old sound, if playing)
2724 * Fixes case with devices that can only play one sound at a time
2725 * Otherwise, other sounds in mid-play will be terminated without warning and in a stuck state
2726 */
2727
2728 if (useGlobalHTML5Audio) {
2729
2730 if (dURL === decodeURI(lastGlobalHTML5URL)) {
2731 // global HTML5 audio: re-use of URL
2732 sameURL = true;
2733 }
2734
2735 } else if (dURL === decodeURI(lastURL)) {
2736
2737 // options URL is the same as the "last" URL, and we used (loaded) it
2738 sameURL = true;
2739
2740 }
2741
2742 if (a) {
2743
2744 if (a._s) {
2745
2746 if (useGlobalHTML5Audio) {
2747
2748 if (a._s && a._s.playState && !sameURL) {
2749
2750 // global HTML5 audio case, and loading a new URL. stop the currently-playing one.
2751 a._s.stop();
2752
2753 }
2754
2755 } else if (!useGlobalHTML5Audio && dURL === decodeURI(lastURL)) {
2756
2757 // non-global HTML5 reuse case: same url, ignore request
2758 s._apply_loop(a, instanceOptions.loops);
2759
2760 return a;
2761
2762 }
2763
2764 }
2765
2766 if (!sameURL) {
2767
2768 // don't retain onPosition() stuff with new URL.
2769
2770 resetProperties(false);
2771
2772 // assign new HTML5 URL
2773
2774 a.src = instanceOptions.url;
2775
2776 s.url = instanceOptions.url;
2777
2778 lastURL = instanceOptions.url;
2779
2780 lastGlobalHTML5URL = instanceOptions.url;
2781
2782 a._called_load = false;
2783
2784 }
2785
2786 } else {
2787
2788 if (instanceOptions.autoLoad || instanceOptions.autoPlay) {
2789
2790 s._a = new Audio(instanceOptions.url);
2791
2792 } else {
2793
2794 // null for stupid Opera 9.64 case
2795 s._a = (isOpera && opera.version() < 10 ? new Audio(null) : new Audio());
2796
2797 }
2798
2799 // assign local reference
2800 a = s._a;
2801
2802 a._called_load = false;
2803
2804 if (useGlobalHTML5Audio) {
2805
2806 globalHTML5Audio = a;
2807
2808 }
2809
2810 }
2811
2812 s.isHTML5 = true;
2813
2814 // store a ref on the track
2815 s._a = a;
2816
2817 // store a ref on the audio
2818 a._s = s;
2819
2820 add_html5_events();
2821
2822 s._apply_loop(a, instanceOptions.loops);
2823
2824 if (instanceOptions.autoLoad || instanceOptions.autoPlay) {
2825
2826 s.load();
2827
2828 } else {
2829
2830 // early HTML5 implementation (non-standard)
2831 a.autobuffer = false;
2832
2833 // standard ('none' is also an option.)
2834 a.preload = 'auto';
2835
2836 }
2837
2838 return a;
2839
2840 };
2841
2842 add_html5_events = function() {
2843
2844 if (s._a._added_events) {
2845 return false;
2846 }
2847
2848 var f;
2849
2850 function add(oEvt, oFn, bCapture) {
2851 return s._a ? s._a.addEventListener(oEvt, oFn, bCapture||false) : null;
2852 }
2853
2854 s._a._added_events = true;
2855
2856 for (f in html5_events) {
2857 if (html5_events.hasOwnProperty(f)) {
2858 add(f, html5_events[f]);
2859 }
2860 }
2861
2862 return true;
2863
2864 };
2865
2866 remove_html5_events = function() {
2867
2868 // Remove event listeners
2869
2870 var f;
2871
2872 function remove(oEvt, oFn, bCapture) {
2873 return (s._a ? s._a.removeEventListener(oEvt, oFn, bCapture||false) : null);
2874 }
2875
2876 sm2._wD(s.id + ': Removing event listeners');
2877 s._a._added_events = false;
2878
2879 for (f in html5_events) {
2880 if (html5_events.hasOwnProperty(f)) {
2881 remove(f, html5_events[f]);
2882 }
2883 }
2884
2885 };
2886
2887 /**
2888 * Pseudo-private event internals
2889 * ------------------------------
2890 */
2891
2892 this._onload = function(nSuccess) {
2893
2894 var fN,
2895 // check for duration to prevent false positives from flash 8 when loading from cache.
2896 loadOK = !!nSuccess || (!s.isHTML5 && fV === 8 && s.duration);
2897
2898 // <d>
2899 fN = s.id + ': ';
2900 sm2._wD(fN + (loadOK ? 'onload()' : 'Failed to load? - ' + s.url), (loadOK ? 1 : 2));
2901 if (!loadOK && !s.isHTML5) {
2902 if (sm2.sandbox.noRemote === true) {
2903 sm2._wD(fN + str('noNet'), 1);
2904 }
2905 if (sm2.sandbox.noLocal === true) {
2906 sm2._wD(fN + str('noLocal'), 1);
2907 }
2908 }
2909 // </d>
2910
2911 s.loaded = loadOK;
2912 s.readyState = loadOK?3:2;
2913 s._onbufferchange(0);
2914
2915 if (s._iO.onload) {
2916 wrapCallback(s, function() {
2917 s._iO.onload.apply(s, [loadOK]);
2918 });
2919 }
2920
2921 return true;
2922
2923 };
2924
2925 this._onbufferchange = function(nIsBuffering) {
2926
2927 if (s.playState === 0) {
2928 // ignore if not playing
2929 return false;
2930 }
2931
2932 if ((nIsBuffering && s.isBuffering) || (!nIsBuffering && !s.isBuffering)) {
2933 return false;
2934 }
2935
2936 s.isBuffering = (nIsBuffering === 1);
2937 if (s._iO.onbufferchange) {
2938 sm2._wD(s.id + ': Buffer state change: ' + nIsBuffering);
2939 s._iO.onbufferchange.apply(s);
2940 }
2941
2942 return true;
2943
2944 };
2945
2946 /**
2947 * Playback may have stopped due to buffering, or related reason.
2948 * This state can be encountered on iOS < 6 when auto-play is blocked.
2949 */
2950
2951 this._onsuspend = function() {
2952
2953 if (s._iO.onsuspend) {
2954 sm2._wD(s.id + ': Playback suspended');
2955 s._iO.onsuspend.apply(s);
2956 }
2957
2958 return true;
2959
2960 };
2961
2962 /**
2963 * flash 9/movieStar + RTMP-only method, should fire only once at most
2964 * at this point we just recreate failed sounds rather than trying to reconnect
2965 */
2966
2967 this._onfailure = function(msg, level, code) {
2968
2969 s.failures++;
2970 sm2._wD(s.id + ': Failures = ' + s.failures);
2971
2972 if (s._iO.onfailure && s.failures === 1) {
2973 s._iO.onfailure(s, msg, level, code);
2974 } else {
2975 sm2._wD(s.id + ': Ignoring failure');
2976 }
2977
2978 };
2979
2980 this._onfinish = function() {
2981
2982 // store local copy before it gets trashed...
2983 var io_onfinish = s._iO.onfinish;
2984
2985 s._onbufferchange(0);
2986 s._resetOnPosition(0);
2987
2988 // reset some state items
2989 if (s.instanceCount) {
2990
2991 s.instanceCount--;
2992
2993 if (!s.instanceCount) {
2994
2995 // remove onPosition listeners, if any
2996 detachOnPosition();
2997
2998 // reset instance options
2999 s.playState = 0;
3000 s.paused = false;
3001 s.instanceCount = 0;
3002 s.instanceOptions = {};
3003 s._iO = {};
3004 stop_html5_timer();
3005
3006 // reset position, too
3007 if (s.isHTML5) {
3008 s.position = 0;
3009 }
3010
3011 }
3012
3013 if (!s.instanceCount || s._iO.multiShotEvents) {
3014 // fire onfinish for last, or every instance
3015 if (io_onfinish) {
3016 sm2._wD(s.id + ': onfinish()');
3017 wrapCallback(s, function() {
3018 io_onfinish.apply(s);
3019 });
3020 }
3021 }
3022
3023 }
3024
3025 };
3026
3027 this._whileloading = function(nBytesLoaded, nBytesTotal, nDuration, nBufferLength) {
3028
3029 var instanceOptions = s._iO;
3030
3031 s.bytesLoaded = nBytesLoaded;
3032 s.bytesTotal = nBytesTotal;
3033 s.duration = Math.floor(nDuration);
3034 s.bufferLength = nBufferLength;
3035
3036 if (!s.isHTML5 && !instanceOptions.isMovieStar) {
3037
3038 if (instanceOptions.duration) {
3039 // use duration from options, if specified and larger. nobody should be specifying duration in options, actually, and it should be retired.
3040 s.durationEstimate = (s.duration > instanceOptions.duration) ? s.duration : instanceOptions.duration;
3041 } else {
3042 s.durationEstimate = parseInt((s.bytesTotal / s.bytesLoaded) * s.duration, 10);
3043 }
3044
3045 } else {
3046
3047 s.durationEstimate = s.duration;
3048
3049 }
3050
3051 // for flash, reflect sequential-load-style buffering
3052 if (!s.isHTML5) {
3053 s.buffered = [{
3054 'start': 0,
3055 'end': s.duration
3056 }];
3057 }
3058
3059 // allow whileloading to fire even if "load" fired under HTML5, due to HTTP range/partials
3060 if ((s.readyState !== 3 || s.isHTML5) && instanceOptions.whileloading) {
3061 instanceOptions.whileloading.apply(s);
3062 }
3063
3064 };
3065
3066 this._whileplaying = function(nPosition, oPeakData, oWaveformDataLeft, oWaveformDataRight, oEQData) {
3067
3068 var instanceOptions = s._iO,
3069 eqLeft;
3070
3071 if (isNaN(nPosition) || nPosition === null) {
3072 // flash safety net
3073 return false;
3074 }
3075
3076 // 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.
3077 s.position = Math.max(0, nPosition);
3078
3079 s._processOnPosition();
3080
3081 if (!s.isHTML5 && fV > 8) {
3082
3083 if (instanceOptions.usePeakData && oPeakData !== _undefined && oPeakData) {
3084 s.peakData = {
3085 left: oPeakData.leftPeak,
3086 right: oPeakData.rightPeak
3087 };
3088 }
3089
3090 if (instanceOptions.useWaveformData && oWaveformDataLeft !== _undefined && oWaveformDataLeft) {
3091 s.waveformData = {
3092 left: oWaveformDataLeft.split(','),
3093 right: oWaveformDataRight.split(',')
3094 };
3095 }
3096
3097 if (instanceOptions.useEQData) {
3098 if (oEQData !== _undefined && oEQData && oEQData.leftEQ) {
3099 eqLeft = oEQData.leftEQ.split(',');
3100 s.eqData = eqLeft;
3101 s.eqData.left = eqLeft;
3102 if (oEQData.rightEQ !== _undefined && oEQData.rightEQ) {
3103 s.eqData.right = oEQData.rightEQ.split(',');
3104 }
3105 }
3106 }
3107
3108 }
3109
3110 if (s.playState === 1) {
3111
3112 // special case/hack: ensure buffering is false if loading from cache (and not yet started)
3113 if (!s.isHTML5 && fV === 8 && !s.position && s.isBuffering) {
3114 s._onbufferchange(0);
3115 }
3116
3117 if (instanceOptions.whileplaying) {
3118 // flash may call after actual finish
3119 instanceOptions.whileplaying.apply(s);
3120 }
3121
3122 }
3123
3124 return true;
3125
3126 };
3127
3128 this._oncaptiondata = function(oData) {
3129
3130 /**
3131 * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature
3132 *
3133 * @param {object} oData
3134 */
3135
3136 sm2._wD(s.id + ': Caption data received.');
3137
3138 s.captiondata = oData;
3139
3140 if (s._iO.oncaptiondata) {
3141 s._iO.oncaptiondata.apply(s, [oData]);
3142 }
3143
3144 };
3145
3146 this._onmetadata = function(oMDProps, oMDData) {
3147
3148 /**
3149 * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature
3150 * RTMP may include song title, MovieStar content may include encoding info
3151 *
3152 * @param {array} oMDProps (names)
3153 * @param {array} oMDData (values)
3154 */
3155
3156 sm2._wD(s.id + ': Metadata received.');
3157
3158 var oData = {}, i, j;
3159
3160 for (i = 0, j = oMDProps.length; i < j; i++) {
3161 oData[oMDProps[i]] = oMDData[i];
3162 }
3163 s.metadata = oData;
3164
3165 if (s._iO.onmetadata) {
3166 s._iO.onmetadata.apply(s);
3167 }
3168
3169 };
3170
3171 this._onid3 = function(oID3Props, oID3Data) {
3172
3173 /**
3174 * internal: flash 8 + flash 9 ID3 feature
3175 * may include artist, song title etc.
3176 *
3177 * @param {array} oID3Props (names)
3178 * @param {array} oID3Data (values)
3179 */
3180
3181 sm2._wD(s.id + ': ID3 data received.');
3182
3183 var oData = [], i, j;
3184
3185 for (i = 0, j = oID3Props.length; i < j; i++) {
3186 oData[oID3Props[i]] = oID3Data[i];
3187 }
3188 s.id3 = mixin(s.id3, oData);
3189
3190 if (s._iO.onid3) {
3191 s._iO.onid3.apply(s);
3192 }
3193
3194 };
3195
3196 // flash/RTMP-only
3197
3198 this._onconnect = function(bSuccess) {
3199
3200 bSuccess = (bSuccess === 1);
3201 sm2._wD(s.id + ': ' + (bSuccess ? 'Connected.' : 'Failed to connect? - ' + s.url), (bSuccess ? 1 : 2));
3202 s.connected = bSuccess;
3203
3204 if (bSuccess) {
3205
3206 s.failures = 0;
3207
3208 if (idCheck(s.id)) {
3209 if (s.getAutoPlay()) {
3210 // only update the play state if auto playing
3211 s.play(_undefined, s.getAutoPlay());
3212 } else if (s._iO.autoLoad) {
3213 s.load();
3214 }
3215 }
3216
3217 if (s._iO.onconnect) {
3218 s._iO.onconnect.apply(s, [bSuccess]);
3219 }
3220
3221 }
3222
3223 };
3224
3225 this._ondataerror = function(sError) {
3226
3227 // flash 9 wave/eq data handler
3228 // hack: called at start, and end from flash at/after onfinish()
3229 if (s.playState > 0) {
3230 sm2._wD(s.id + ': Data error: ' + sError);
3231 if (s._iO.ondataerror) {
3232 s._iO.ondataerror.apply(s);
3233 }
3234 }
3235
3236 };
3237
3238 // <d>
3239 this._debug();
3240 // </d>
3241
3242 }; // SMSound()
3243
3244 /**
3245 * Private SoundManager internals
3246 * ------------------------------
3247 */
3248
3249 getDocument = function() {
3250
3251 return (doc.body || doc._docElement || doc.getElementsByTagName('div')[0]);
3252
3253 };
3254
3255 id = function(sID) {
3256
3257 return doc.getElementById(sID);
3258
3259 };
3260
3261 mixin = function(oMain, oAdd) {
3262
3263 // non-destructive merge
3264 var o1 = (oMain || {}), o2, o;
3265
3266 // if unspecified, o2 is the default options object
3267 o2 = (oAdd === _undefined ? sm2.defaultOptions : oAdd);
3268
3269 for (o in o2) {
3270
3271 if (o2.hasOwnProperty(o) && o1[o] === _undefined) {
3272
3273 if (typeof o2[o] !== 'object' || o2[o] === null) {
3274
3275 // assign directly
3276 o1[o] = o2[o];
3277
3278 } else {
3279
3280 // recurse through o2
3281 o1[o] = mixin(o1[o], o2[o]);
3282
3283 }
3284
3285 }
3286
3287 }
3288
3289 return o1;
3290
3291 };
3292
3293 wrapCallback = function(oSound, callback) {
3294
3295 /**
3296 * 03/03/2013: Fix for Flash Player 11.6.602.171 + Flash 8 (flashVersion = 8) SWF issue
3297 * setTimeout() fix for certain SMSound callbacks like onload() and onfinish(), where subsequent calls like play() and load() fail when Flash Player 11.6.602.171 is installed, and using soundManager with flashVersion = 8 (which is the default).
3298 * Not sure of exact cause. Suspect race condition and/or invalid (NaN-style) position argument trickling down to the next JS -> Flash _start() call, in the play() case.
3299 * Fix: setTimeout() to yield, plus safer null / NaN checking on position argument provided to Flash.
3300 * https://getsatisfaction.com/schillmania/topics/recent_chrome_update_seems_to_have_broken_my_sm2_audio_player
3301 */
3302 if (!oSound.isHTML5 && fV === 8) {
3303 window.setTimeout(callback, 0);
3304 } else {
3305 callback();
3306 }
3307
3308 };
3309
3310 // additional soundManager properties that soundManager.setup() will accept
3311
3312 extraOptions = {
3313 'onready': 1,
3314 'ontimeout': 1,
3315 'defaultOptions': 1,
3316 'flash9Options': 1,
3317 'movieStarOptions': 1
3318 };
3319
3320 assign = function(o, oParent) {
3321
3322 /**
3323 * recursive assignment of properties, soundManager.setup() helper
3324 * allows property assignment based on whitelist
3325 */
3326
3327 var i,
3328 result = true,
3329 hasParent = (oParent !== _undefined),
3330 setupOptions = sm2.setupOptions,
3331 bonusOptions = extraOptions;
3332
3333 // <d>
3334
3335 // if soundManager.setup() called, show accepted parameters.
3336
3337 if (o === _undefined) {
3338
3339 result = [];
3340
3341 for (i in setupOptions) {
3342
3343 if (setupOptions.hasOwnProperty(i)) {
3344 result.push(i);
3345 }
3346
3347 }
3348
3349 for (i in bonusOptions) {
3350
3351 if (bonusOptions.hasOwnProperty(i)) {
3352
3353 if (typeof sm2[i] === 'object') {
3354
3355 result.push(i+': {...}');
3356
3357 } else if (sm2[i] instanceof Function) {
3358
3359 result.push(i+': function() {...}');
3360
3361 } else {
3362
3363 result.push(i);
3364
3365 }
3366
3367 }
3368
3369 }
3370
3371 sm2._wD(str('setup', result.join(', ')));
3372
3373 return false;
3374
3375 }
3376
3377 // </d>
3378
3379 for (i in o) {
3380
3381 if (o.hasOwnProperty(i)) {
3382
3383 // if not an {object} we want to recurse through...
3384
3385 if (typeof o[i] !== 'object' || o[i] === null || o[i] instanceof Array || o[i] instanceof RegExp) {
3386
3387 // check "allowed" options
3388
3389 if (hasParent && bonusOptions[oParent] !== _undefined) {
3390
3391 // valid recursive / nested object option, eg., { defaultOptions: { volume: 50 } }
3392 sm2[oParent][i] = o[i];
3393
3394 } else if (setupOptions[i] !== _undefined) {
3395
3396 // special case: assign to setupOptions object, which soundManager property references
3397 sm2.setupOptions[i] = o[i];
3398
3399 // assign directly to soundManager, too
3400 sm2[i] = o[i];
3401
3402 } else if (bonusOptions[i] === _undefined) {
3403
3404 // invalid or disallowed parameter. complain.
3405 complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2);
3406
3407 result = false;
3408
3409 } else {
3410
3411 /**
3412 * valid extraOptions (bonusOptions) parameter.
3413 * is it a method, like onready/ontimeout? call it.
3414 * multiple parameters should be in an array, eg. soundManager.setup({onready: [myHandler, myScope]});
3415 */
3416
3417 if (sm2[i] instanceof Function) {
3418
3419 sm2[i].apply(sm2, (o[i] instanceof Array? o[i] : [o[i]]));
3420
3421 } else {
3422
3423 // good old-fashioned direct assignment
3424 sm2[i] = o[i];
3425
3426 }
3427
3428 }
3429
3430 } else {
3431
3432 // recursion case, eg., { defaultOptions: { ... } }
3433
3434 if (bonusOptions[i] === _undefined) {
3435
3436 // invalid or disallowed parameter. complain.
3437 complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2);
3438
3439 result = false;
3440
3441 } else {
3442
3443 // recurse through object
3444 return assign(o[i], i);
3445
3446 }
3447
3448 }
3449
3450 }
3451
3452 }
3453
3454 return result;
3455
3456 };
3457
3458 function preferFlashCheck(kind) {
3459
3460 // whether flash should play a given type
3461 return (sm2.preferFlash && hasFlash && !sm2.ignoreFlash && (sm2.flash[kind] !== _undefined && sm2.flash[kind]));
3462
3463 }
3464
3465 /**
3466 * Internal DOM2-level event helpers
3467 * ---------------------------------
3468 */
3469
3470 event = (function() {
3471
3472 // normalize event methods
3473 var old = (window.attachEvent),
3474 evt = {
3475 add: (old?'attachEvent':'addEventListener'),
3476 remove: (old?'detachEvent':'removeEventListener')
3477 };
3478
3479 // normalize "on" event prefix, optional capture argument
3480 function getArgs(oArgs) {
3481
3482 var args = slice.call(oArgs),
3483 len = args.length;
3484
3485 if (old) {
3486 // prefix
3487 args[1] = 'on' + args[1];
3488 if (len > 3) {
3489 // no capture
3490 args.pop();
3491 }
3492 } else if (len === 3) {
3493 args.push(false);
3494 }
3495
3496 return args;
3497
3498 }
3499
3500 function apply(args, sType) {
3501
3502 // normalize and call the event method, with the proper arguments
3503 var element = args.shift(),
3504 method = [evt[sType]];
3505
3506 if (old) {
3507 // old IE can't do apply().
3508 element[method](args[0], args[1]);
3509 } else {
3510 element[method].apply(element, args);
3511 }
3512
3513 }
3514
3515 function add() {
3516
3517 apply(getArgs(arguments), 'add');
3518
3519 }
3520
3521 function remove() {
3522
3523 apply(getArgs(arguments), 'remove');
3524
3525 }
3526
3527 return {
3528 'add': add,
3529 'remove': remove
3530 };
3531
3532 }());
3533
3534 /**
3535 * Internal HTML5 event handling
3536 * -----------------------------
3537 */
3538
3539 function html5_event(oFn) {
3540
3541 // wrap html5 event handlers so we don't call them on destroyed and/or unloaded sounds
3542
3543 return function(e) {
3544
3545 var s = this._s,
3546 result;
3547
3548 if (!s || !s._a) {
3549 // <d>
3550 if (s && s.id) {
3551 sm2._wD(s.id + ': Ignoring ' + e.type);
3552 } else {
3553 sm2._wD(h5 + 'Ignoring ' + e.type);
3554 }
3555 // </d>
3556 result = null;
3557 } else {
3558 result = oFn.call(this, e);
3559 }
3560
3561 return result;
3562
3563 };
3564
3565 }
3566
3567 html5_events = {
3568
3569 // HTML5 event-name-to-handler map
3570
3571 abort: html5_event(function() {
3572
3573 sm2._wD(this._s.id + ': abort');
3574
3575 }),
3576
3577 // enough has loaded to play
3578
3579 canplay: html5_event(function() {
3580
3581 var s = this._s,
3582 position1K;
3583
3584 if (s._html5_canplay) {
3585 // this event has already fired. ignore.
3586 return true;
3587 }
3588
3589 s._html5_canplay = true;
3590 sm2._wD(s.id + ': canplay');
3591 s._onbufferchange(0);
3592
3593 // position according to instance options
3594 position1K = (s._iO.position !== _undefined && !isNaN(s._iO.position)?s._iO.position/1000:null);
3595
3596 // set the position if position was set before the sound loaded
3597 if (s.position && this.currentTime !== position1K) {
3598 sm2._wD(s.id + ': canplay: Setting position to ' + position1K);
3599 try {
3600 this.currentTime = position1K;
3601 } catch(ee) {
3602 sm2._wD(s.id + ': canplay: Setting position of ' + position1K + ' failed: ' + ee.message, 2);
3603 }
3604 }
3605
3606 // hack for HTML5 from/to case
3607 if (s._iO._oncanplay) {
3608 s._iO._oncanplay();
3609 }
3610
3611 }),
3612
3613 canplaythrough: html5_event(function() {
3614
3615 var s = this._s;
3616
3617 if (!s.loaded) {
3618 s._onbufferchange(0);
3619 s._whileloading(s.bytesLoaded, s.bytesTotal, s._get_html5_duration());
3620 s._onload(true);
3621 }
3622
3623 }),
3624
3625 // TODO: Reserved for potential use
3626 /*
3627 emptied: html5_event(function() {
3628
3629 sm2._wD(this._s.id + ': emptied');
3630
3631 }),
3632 */
3633
3634 ended: html5_event(function() {
3635
3636 var s = this._s;
3637
3638 sm2._wD(s.id + ': ended');
3639
3640 s._onfinish();
3641
3642 }),
3643
3644 error: html5_event(function() {
3645
3646 sm2._wD(this._s.id + ': HTML5 error, code ' + this.error.code);
3647 // call load with error state?
3648 this._s._onload(false);
3649
3650 }),
3651
3652 loadeddata: html5_event(function() {
3653
3654 var s = this._s;
3655
3656 sm2._wD(s.id + ': loadeddata');
3657
3658 // safari seems to nicely report progress events, eventually totalling 100%
3659 if (!s._loaded && !isSafari) {
3660 s.duration = s._get_html5_duration();
3661 }
3662
3663 }),
3664
3665 loadedmetadata: html5_event(function() {
3666
3667 sm2._wD(this._s.id + ': loadedmetadata');
3668
3669 }),
3670
3671 loadstart: html5_event(function() {
3672
3673 sm2._wD(this._s.id + ': loadstart');
3674 // assume buffering at first
3675 this._s._onbufferchange(1);
3676
3677 }),
3678
3679 play: html5_event(function() {
3680
3681 sm2._wD(this._s.id + ': play()');
3682 // once play starts, no buffering
3683 this._s._onbufferchange(0);
3684
3685 }),
3686
3687 playing: html5_event(function() {
3688
3689 sm2._wD(this._s.id + ': playing');
3690 // once play starts, no buffering
3691 this._s._onbufferchange(0);
3692
3693 }),
3694
3695 progress: html5_event(function(e) {
3696
3697 // note: can fire repeatedly after "loaded" event, due to use of HTTP range/partials
3698
3699 var s = this._s,
3700 i, j, str, buffered = 0,
3701 isProgress = (e.type === 'progress'),
3702 ranges = e.target.buffered,
3703 // firefox 3.6 implements e.loaded/total (bytes)
3704 loaded = (e.loaded||0),
3705 total = (e.total||1),
3706 // HTML5 returns msec. SM2 API uses seconds for setPosition() etc., whether Flash or HTML5.
3707 scale = 1000;
3708
3709 // reset the "buffered" (loaded byte ranges) array
3710 s.buffered = [];
3711
3712 if (ranges && ranges.length) {
3713
3714 // if loaded is 0, try TimeRanges implementation as % of load
3715 // https://developer.mozilla.org/en/DOM/TimeRanges
3716
3717 // re-build "buffered" array
3718 for (i=0, j=ranges.length; i<j; i++) {
3719 s.buffered.push({
3720 'start': ranges.start(i) * scale,
3721 'end': ranges.end(i) * scale
3722 });
3723 }
3724
3725 // use the last value locally
3726 buffered = (ranges.end(0) - ranges.start(0)) * scale;
3727
3728 // linear case, buffer sum; does not account for seeking and HTTP partials / byte ranges
3729 loaded = buffered/(e.target.duration*scale);
3730
3731 // <d>
3732 if (isProgress && ranges.length > 1) {
3733 str = [];
3734 j = ranges.length;
3735 for (i=0; i<j; i++) {
3736 str.push(e.target.buffered.start(i)*scale +'-'+ e.target.buffered.end(i)*scale);
3737 }
3738 sm2._wD(this._s.id + ': progress, timeRanges: ' + str.join(', '));
3739 }
3740
3741 if (isProgress && !isNaN(loaded)) {
3742 sm2._wD(this._s.id + ': progress, ' + Math.floor(loaded*100) + '% loaded');
3743 }
3744 // </d>
3745
3746 }
3747
3748 if (!isNaN(loaded)) {
3749
3750 // if progress, likely not buffering
3751 s._onbufferchange(0);
3752 // TODO: prevent calls with duplicate values.
3753 s._whileloading(loaded, total, s._get_html5_duration());
3754 if (loaded && total && loaded === total) {
3755 // in case "onload" doesn't fire (eg. gecko 1.9.2)
3756 html5_events.canplaythrough.call(this, e);
3757 }
3758
3759 }
3760
3761 }),
3762
3763 ratechange: html5_event(function() {
3764
3765 sm2._wD(this._s.id + ': ratechange');
3766
3767 }),
3768
3769 suspend: html5_event(function(e) {
3770
3771 // download paused/stopped, may have finished (eg. onload)
3772 var s = this._s;
3773
3774 sm2._wD(this._s.id + ': suspend');
3775 html5_events.progress.call(this, e);
3776 s._onsuspend();
3777
3778 }),
3779
3780 stalled: html5_event(function() {
3781
3782 sm2._wD(this._s.id + ': stalled');
3783
3784 }),
3785
3786 timeupdate: html5_event(function() {
3787
3788 this._s._onTimer();
3789
3790 }),
3791
3792 waiting: html5_event(function() {
3793
3794 var s = this._s;
3795
3796 // see also: seeking
3797 sm2._wD(this._s.id + ': waiting');
3798
3799 // playback faster than download rate, etc.
3800 s._onbufferchange(1);
3801
3802 })
3803
3804 };
3805
3806 html5OK = function(iO) {
3807
3808 // playability test based on URL or MIME type
3809
3810 var result;
3811
3812 if (iO.serverURL || (iO.type && preferFlashCheck(iO.type))) {
3813
3814 // RTMP, or preferring flash
3815 result = false;
3816
3817 } else {
3818
3819 // Use type, if specified. If HTML5-only mode, no other options, so just give 'er
3820 result = ((iO.type ? html5CanPlay({type:iO.type}) : html5CanPlay({url:iO.url}) || sm2.html5Only));
3821
3822 }
3823
3824 return result;
3825
3826 };
3827
3828 html5Unload = function(oAudio, url) {
3829
3830 /**
3831 * Internal method: Unload media, and cancel any current/pending network requests.
3832 * Firefox can load an empty URL, which allegedly destroys the decoder and stops the download.
3833 * https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Stopping_the_download_of_media
3834 * However, Firefox has been seen loading a relative URL from '' and thus requesting the hosting page on unload.
3835 * Other UA behaviour is unclear, so everyone else gets an about:blank-style URL.
3836 */
3837
3838 if (oAudio) {
3839
3840 // Firefox likes '' for unload (used to work?) - however, may request hosting page URL (bad.) Most other UAs dislike '' and fail to unload.
3841 oAudio.src = url;
3842
3843 // reset some state, too
3844 oAudio._called_load = false;
3845
3846 }
3847
3848 if (useGlobalHTML5Audio) {
3849
3850 // ensure URL state is trashed, also
3851 lastGlobalHTML5URL = null;
3852
3853 }
3854
3855 };
3856
3857 html5CanPlay = function(o) {
3858
3859 /**
3860 * Try to find MIME, test and return truthiness
3861 * o = {
3862 * url: '/path/to/an.mp3',
3863 * type: 'audio/mp3'
3864 * }
3865 */
3866
3867 if (!sm2.useHTML5Audio || !sm2.hasHTML5) {
3868 return false;
3869 }
3870
3871 var url = (o.url || null),
3872 mime = (o.type || null),
3873 aF = sm2.audioFormats,
3874 result,
3875 offset,
3876 fileExt,
3877 item;
3878
3879 // account for known cases like audio/mp3
3880
3881 if (mime && sm2.html5[mime] !== _undefined) {
3882 return (sm2.html5[mime] && !preferFlashCheck(mime));
3883 }
3884
3885 if (!html5Ext) {
3886 html5Ext = [];
3887 for (item in aF) {
3888 if (aF.hasOwnProperty(item)) {
3889 html5Ext.push(item);
3890 if (aF[item].related) {
3891 html5Ext = html5Ext.concat(aF[item].related);
3892 }
3893 }
3894 }
3895 html5Ext = new RegExp('\\.('+html5Ext.join('|')+')(\\?.*)?$','i');
3896 }
3897
3898 // TODO: Strip URL queries, etc.
3899 fileExt = (url ? url.toLowerCase().match(html5Ext) : null);
3900
3901 if (!fileExt || !fileExt.length) {
3902 if (!mime) {
3903 result = false;
3904 } else {
3905 // audio/mp3 -> mp3, result should be known
3906 offset = mime.indexOf(';');
3907 // strip "audio/X; codecs..."
3908 fileExt = (offset !== -1?mime.substr(0,offset):mime).substr(6);
3909 }
3910 } else {
3911 // match the raw extension name - "mp3", for example
3912 fileExt = fileExt[1];
3913 }
3914
3915 if (fileExt && sm2.html5[fileExt] !== _undefined) {
3916 // result known
3917 result = (sm2.html5[fileExt] && !preferFlashCheck(fileExt));
3918 } else {
3919 mime = 'audio/'+fileExt;
3920 result = sm2.html5.canPlayType({type:mime});
3921 sm2.html5[fileExt] = result;
3922 // sm2._wD('canPlayType, found result: ' + result);
3923 result = (result && sm2.html5[mime] && !preferFlashCheck(mime));
3924 }
3925
3926 return result;
3927
3928 };
3929
3930 testHTML5 = function() {
3931
3932 /**
3933 * Internal: Iterates over audioFormats, determining support eg. audio/mp3, audio/mpeg and so on
3934 * assigns results to html5[] and flash[].
3935 */
3936
3937 if (!sm2.useHTML5Audio || !sm2.hasHTML5) {
3938 return false;
3939 }
3940
3941 // 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. :/
3942 var a = (Audio !== _undefined ? (isOpera && opera.version() < 10 ? new Audio(null) : new Audio()) : null),
3943 item, lookup, support = {}, aF, i;
3944
3945 function cp(m) {
3946
3947 var canPlay, i, j,
3948 result = false,
3949 isOK = false;
3950
3951 if (!a || typeof a.canPlayType !== 'function') {
3952 return result;
3953 }
3954
3955 if (m instanceof Array) {
3956 // iterate through all mime types, return any successes
3957 for (i=0, j=m.length; i<j; i++) {
3958 if (sm2.html5[m[i]] || a.canPlayType(m[i]).match(sm2.html5Test)) {
3959 isOK = true;
3960 sm2.html5[m[i]] = true;
3961 // note flash support, too
3962 sm2.flash[m[i]] = !!(m[i].match(flashMIME));
3963 }
3964 }
3965 result = isOK;
3966 } else {
3967 canPlay = (a && typeof a.canPlayType === 'function' ? a.canPlayType(m) : false);
3968 result = !!(canPlay && (canPlay.match(sm2.html5Test)));
3969 }
3970
3971 return result;
3972
3973 }
3974
3975 // test all registered formats + codecs
3976
3977 aF = sm2.audioFormats;
3978
3979 for (item in aF) {
3980
3981 if (aF.hasOwnProperty(item)) {
3982
3983 lookup = 'audio/' + item;
3984
3985 support[item] = cp(aF[item].type);
3986
3987 // write back generic type too, eg. audio/mp3
3988 support[lookup] = support[item];
3989
3990 // assign flash
3991 if (item.match(flashMIME)) {
3992
3993 sm2.flash[item] = true;
3994 sm2.flash[lookup] = true;
3995
3996 } else {
3997
3998 sm2.flash[item] = false;
3999 sm2.flash[lookup] = false;
4000
4001 }
4002
4003 // assign result to related formats, too
4004
4005 if (aF[item] && aF[item].related) {
4006
4007 for (i=aF[item].related.length-1; i >= 0; i--) {
4008
4009 // eg. audio/m4a
4010 support['audio/'+aF[item].related[i]] = support[item];
4011 sm2.html5[aF[item].related[i]] = support[item];
4012 sm2.flash[aF[item].related[i]] = support[item];
4013
4014 }
4015
4016 }
4017
4018 }
4019
4020 }
4021
4022 support.canPlayType = (a?cp:null);
4023 sm2.html5 = mixin(sm2.html5, support);
4024
4025 return true;
4026
4027 };
4028
4029 strings = {
4030
4031 // <d>
4032 notReady: 'Unavailable - wait until onready() has fired.',
4033 notOK: 'Audio support is not available.',
4034 domError: sm + 'exception caught while appending SWF to DOM.',
4035 spcWmode: 'Removing wmode, preventing known SWF loading issue(s)',
4036 swf404: smc + 'Verify that %s is a valid path.',
4037 tryDebug: 'Try ' + sm + '.debugFlash = true for more security details (output goes to SWF.)',
4038 checkSWF: 'See SWF output for more debug info.',
4039 localFail: smc + '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/',
4040 waitFocus: smc + 'Special case: Waiting for SWF to load with window focus...',
4041 waitForever: smc + 'Waiting indefinitely for Flash (will recover if unblocked)...',
4042 waitSWF: smc + 'Waiting for 100% SWF load...',
4043 needFunction: smc + 'Function object expected for %s',
4044 badID: 'Warning: Sound ID "%s" should be a string, starting with a non-numeric character',
4045 currentObj: smc + '_debug(): Current sound objects',
4046 waitOnload: smc + 'Waiting for window.onload()',
4047 docLoaded: smc + 'Document already loaded',
4048 onload: smc + 'initComplete(): calling soundManager.onload()',
4049 onloadOK: sm + '.onload() complete',
4050 didInit: smc + 'init(): Already called?',
4051 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',
4052 badRemove: smc + 'Failed to remove Flash node.',
4053 shutdown: sm + '.disable(): Shutting down',
4054 queue: smc + 'Queueing %s handler',
4055 smError: 'SMSound.load(): Exception: JS-Flash communication failed, or JS error.',
4056 fbTimeout: 'No flash response, applying .'+swfCSS.swfTimedout+' CSS...',
4057 fbLoaded: 'Flash loaded',
4058 flRemoved: smc + 'Flash movie removed.',
4059 fbHandler: smc + 'flashBlockHandler()',
4060 manURL: 'SMSound.load(): Using manually-assigned URL',
4061 onURL: sm + '.load(): current URL already assigned.',
4062 badFV: sm + '.flashVersion must be 8 or 9. "%s" is invalid. Reverting to %s.',
4063 as2loop: 'Note: Setting stream:false so looping can work (flash 8 limitation)',
4064 noNSLoop: 'Note: Looping not implemented for MovieStar formats',
4065 needfl9: 'Note: Switching to flash 9, required for MP4 formats.',
4066 mfTimeout: 'Setting flashLoadTimeout = 0 (infinite) for off-screen, mobile flash case',
4067 needFlash: smc + 'Fatal error: Flash is needed to play some required formats, but is not available.',
4068 gotFocus: smc + 'Got window focus.',
4069 policy: 'Enabling usePolicyFile for data access',
4070 setup: sm + '.setup(): allowed parameters: %s',
4071 setupError: sm + '.setup(): "%s" cannot be assigned with this method.',
4072 setupUndef: sm + '.setup(): Could not find option "%s"',
4073 setupLate: sm + '.setup(): url, flashVersion and html5Test property changes will not take effect until reboot().',
4074 noURL: smc + 'Flash URL required. Call soundManager.setup({url:...}) to get started.',
4075 sm2Loaded: 'SoundManager 2: Ready.',
4076 reset: sm + '.reset(): Removing event callbacks',
4077 mobileUA: 'Mobile UA detected, preferring HTML5 by default.',
4078 globalHTML5: 'Using singleton HTML5 Audio() pattern for this device.'
4079 // </d>
4080
4081 };
4082
4083 str = function() {
4084
4085 // internal string replace helper.
4086 // arguments: o [,items to replace]
4087 // <d>
4088
4089 // real array, please
4090 var args = slice.call(arguments),
4091
4092 // first arg
4093 o = args.shift(),
4094
4095 str = (strings && strings[o]?strings[o]:''), i, j;
4096 if (str && args && args.length) {
4097 for (i = 0, j = args.length; i < j; i++) {
4098 str = str.replace('%s', args[i]);
4099 }
4100 }
4101
4102 return str;
4103 // </d>
4104
4105 };
4106
4107 loopFix = function(sOpt) {
4108
4109 // flash 8 requires stream = false for looping to work
4110 if (fV === 8 && sOpt.loops > 1 && sOpt.stream) {
4111 _wDS('as2loop');
4112 sOpt.stream = false;
4113 }
4114
4115 return sOpt;
4116
4117 };
4118
4119 policyFix = function(sOpt, sPre) {
4120
4121 if (sOpt && !sOpt.usePolicyFile && (sOpt.onid3 || sOpt.usePeakData || sOpt.useWaveformData || sOpt.useEQData)) {
4122 sm2._wD((sPre || '') + str('policy'));
4123 sOpt.usePolicyFile = true;
4124 }
4125
4126 return sOpt;
4127
4128 };
4129
4130 complain = function(sMsg) {
4131
4132 // <d>
4133 if (console !== _undefined && console.warn !== _undefined) {
4134 console.warn(sMsg);
4135 } else {
4136 sm2._wD(sMsg);
4137 }
4138 // </d>
4139
4140 };
4141
4142 doNothing = function() {
4143
4144 return false;
4145
4146 };
4147
4148 disableObject = function(o) {
4149
4150 var oProp;
4151
4152 for (oProp in o) {
4153 if (o.hasOwnProperty(oProp) && typeof o[oProp] === 'function') {
4154 o[oProp] = doNothing;
4155 }
4156 }
4157
4158 oProp = null;
4159
4160 };
4161
4162 failSafely = function(bNoDisable) {
4163
4164 // general failure exception handler
4165
4166 if (bNoDisable === _undefined) {
4167 bNoDisable = false;
4168 }
4169
4170 if (disabled || bNoDisable) {
4171 sm2.disable(bNoDisable);
4172 }
4173
4174 };
4175
4176 normalizeMovieURL = function(smURL) {
4177
4178 var urlParams = null, url;
4179
4180 if (smURL) {
4181 if (smURL.match(/\.swf(\?.*)?$/i)) {
4182 urlParams = smURL.substr(smURL.toLowerCase().lastIndexOf('.swf?') + 4);
4183 if (urlParams) {
4184 // assume user knows what they're doing
4185 return smURL;
4186 }
4187 } else if (smURL.lastIndexOf('/') !== smURL.length - 1) {
4188 // append trailing slash, if needed
4189 smURL += '/';
4190 }
4191 }
4192
4193 url = (smURL && smURL.lastIndexOf('/') !== - 1 ? smURL.substr(0, smURL.lastIndexOf('/') + 1) : './') + sm2.movieURL;
4194
4195 if (sm2.noSWFCache) {
4196 url += ('?ts=' + new Date().getTime());
4197 }
4198
4199 return url;
4200
4201 };
4202
4203 setVersionInfo = function() {
4204
4205 // short-hand for internal use
4206
4207 fV = parseInt(sm2.flashVersion, 10);
4208
4209 if (fV !== 8 && fV !== 9) {
4210 sm2._wD(str('badFV', fV, defaultFlashVersion));
4211 sm2.flashVersion = fV = defaultFlashVersion;
4212 }
4213
4214 // debug flash movie, if applicable
4215
4216 var isDebug = (sm2.debugMode || sm2.debugFlash?'_debug.swf':'.swf');
4217
4218 if (sm2.useHTML5Audio && !sm2.html5Only && sm2.audioFormats.mp4.required && fV < 9) {
4219 sm2._wD(str('needfl9'));
4220 sm2.flashVersion = fV = 9;
4221 }
4222
4223 sm2.version = sm2.versionNumber + (sm2.html5Only?' (HTML5-only mode)':(fV === 9?' (AS3/Flash 9)':' (AS2/Flash 8)'));
4224
4225 // set up default options
4226 if (fV > 8) {
4227 // +flash 9 base options
4228 sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.flash9Options);
4229 sm2.features.buffering = true;
4230 // +moviestar support
4231 sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.movieStarOptions);
4232 sm2.filePatterns.flash9 = new RegExp('\\.(mp3|' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i');
4233 sm2.features.movieStar = true;
4234 } else {
4235 sm2.features.movieStar = false;
4236 }
4237
4238 // regExp for flash canPlay(), etc.
4239 sm2.filePattern = sm2.filePatterns[(fV !== 8?'flash9':'flash8')];
4240
4241 // if applicable, use _debug versions of SWFs
4242 sm2.movieURL = (fV === 8?'soundmanager2.swf':'soundmanager2_flash9.swf').replace('.swf', isDebug);
4243
4244 sm2.features.peakData = sm2.features.waveformData = sm2.features.eqData = (fV > 8);
4245
4246 };
4247
4248 setPolling = function(bPolling, bHighPerformance) {
4249
4250 if (!flash) {
4251 return false;
4252 }
4253
4254 flash._setPolling(bPolling, bHighPerformance);
4255
4256 };
4257
4258 initDebug = function() {
4259
4260 // starts debug mode, creating output <div> for UAs without console object
4261
4262 // allow force of debug mode via URL
4263 if (sm2.debugURLParam.test(wl)) {
4264 sm2.debugMode = true;
4265 }
4266
4267 // <d>
4268 if (id(sm2.debugID)) {
4269 return false;
4270 }
4271
4272 var oD, oDebug, oTarget, oToggle, tmp;
4273
4274 if (sm2.debugMode && !id(sm2.debugID) && (!hasConsole || !sm2.useConsole || !sm2.consoleOnly)) {
4275
4276 oD = doc.createElement('div');
4277 oD.id = sm2.debugID + '-toggle';
4278
4279 oToggle = {
4280 'position': 'fixed',
4281 'bottom': '0px',
4282 'right': '0px',
4283 'width': '1.2em',
4284 'height': '1.2em',
4285 'lineHeight': '1.2em',
4286 'margin': '2px',
4287 'textAlign': 'center',
4288 'border': '1px solid #999',
4289 'cursor': 'pointer',
4290 'background': '#fff',
4291 'color': '#333',
4292 'zIndex': 10001
4293 };
4294
4295 oD.appendChild(doc.createTextNode('-'));
4296 oD.onclick = toggleDebug;
4297 oD.title = 'Toggle SM2 debug console';
4298
4299 if (ua.match(/msie 6/i)) {
4300 oD.style.position = 'absolute';
4301 oD.style.cursor = 'hand';
4302 }
4303
4304 for (tmp in oToggle) {
4305 if (oToggle.hasOwnProperty(tmp)) {
4306 oD.style[tmp] = oToggle[tmp];
4307 }
4308 }
4309
4310 oDebug = doc.createElement('div');
4311 oDebug.id = sm2.debugID;
4312 oDebug.style.display = (sm2.debugMode?'block':'none');
4313
4314 if (sm2.debugMode && !id(oD.id)) {
4315 try {
4316 oTarget = getDocument();
4317 oTarget.appendChild(oD);
4318 } catch(e2) {
4319 throw new Error(str('domError')+' \n'+e2.toString());
4320 }
4321 oTarget.appendChild(oDebug);
4322 }
4323
4324 }
4325
4326 oTarget = null;
4327 // </d>
4328
4329 };
4330
4331 idCheck = this.getSoundById;
4332
4333 // <d>
4334 _wDS = function(o, errorLevel) {
4335
4336 return (!o ? '' : sm2._wD(str(o), errorLevel));
4337
4338 };
4339
4340 toggleDebug = function() {
4341
4342 var o = id(sm2.debugID),
4343 oT = id(sm2.debugID + '-toggle');
4344
4345 if (!o) {
4346 return false;
4347 }
4348
4349 if (debugOpen) {
4350 // minimize
4351 oT.innerHTML = '+';
4352 o.style.display = 'none';
4353 } else {
4354 oT.innerHTML = '-';
4355 o.style.display = 'block';
4356 }
4357
4358 debugOpen = !debugOpen;
4359
4360 };
4361
4362 debugTS = function(sEventType, bSuccess, sMessage) {
4363
4364 // troubleshooter debug hooks
4365
4366 if (window.sm2Debugger !== _undefined) {
4367 try {
4368 sm2Debugger.handleEvent(sEventType, bSuccess, sMessage);
4369 } catch(e) {
4370 // oh well
4371 }
4372 }
4373
4374 return true;
4375
4376 };
4377 // </d>
4378
4379 getSWFCSS = function() {
4380
4381 var css = [];
4382
4383 if (sm2.debugMode) {
4384 css.push(swfCSS.sm2Debug);
4385 }
4386
4387 if (sm2.debugFlash) {
4388 css.push(swfCSS.flashDebug);
4389 }
4390
4391 if (sm2.useHighPerformance) {
4392 css.push(swfCSS.highPerf);
4393 }
4394
4395 return css.join(' ');
4396
4397 };
4398
4399 flashBlockHandler = function() {
4400
4401 // *possible* flash block situation.
4402
4403 var name = str('fbHandler'),
4404 p = sm2.getMoviePercent(),
4405 css = swfCSS,
4406 error = {type:'FLASHBLOCK'};
4407
4408 if (sm2.html5Only) {
4409 return false;
4410 }
4411
4412 if (!sm2.ok()) {
4413
4414 if (needsFlash) {
4415 // make the movie more visible, so user can fix
4416 sm2.oMC.className = getSWFCSS() + ' ' + css.swfDefault + ' ' + (p === null?css.swfTimedout:css.swfError);
4417 sm2._wD(name + ': ' + str('fbTimeout') + (p ? ' (' + str('fbLoaded') + ')' : ''));
4418 }
4419
4420 sm2.didFlashBlock = true;
4421
4422 // fire onready(), complain lightly
4423 processOnEvents({type:'ontimeout', ignoreInit:true, error:error});
4424 catchError(error);
4425
4426 } else {
4427
4428 // SM2 loaded OK (or recovered)
4429
4430 // <d>
4431 if (sm2.didFlashBlock) {
4432 sm2._wD(name + ': Unblocked');
4433 }
4434 // </d>
4435
4436 if (sm2.oMC) {
4437 sm2.oMC.className = [getSWFCSS(), css.swfDefault, css.swfLoaded + (sm2.didFlashBlock?' '+css.swfUnblocked:'')].join(' ');
4438 }
4439
4440 }
4441
4442 };
4443
4444 addOnEvent = function(sType, oMethod, oScope) {
4445
4446 if (on_queue[sType] === _undefined) {
4447 on_queue[sType] = [];
4448 }
4449
4450 on_queue[sType].push({
4451 'method': oMethod,
4452 'scope': (oScope || null),
4453 'fired': false
4454 });
4455
4456 };
4457
4458 processOnEvents = function(oOptions) {
4459
4460 // if unspecified, assume OK/error
4461
4462 if (!oOptions) {
4463 oOptions = {
4464 type: (sm2.ok() ? 'onready' : 'ontimeout')
4465 };
4466 }
4467
4468 if (!didInit && oOptions && !oOptions.ignoreInit) {
4469 // not ready yet.
4470 return false;
4471 }
4472
4473 if (oOptions.type === 'ontimeout' && (sm2.ok() || (disabled && !oOptions.ignoreInit))) {
4474 // invalid case
4475 return false;
4476 }
4477
4478 var status = {
4479 success: (oOptions && oOptions.ignoreInit?sm2.ok():!disabled)
4480 },
4481
4482 // queue specified by type, or none
4483 srcQueue = (oOptions && oOptions.type?on_queue[oOptions.type]||[]:[]),
4484
4485 queue = [], i, j,
4486 args = [status],
4487 canRetry = (needsFlash && !sm2.ok());
4488
4489 if (oOptions.error) {
4490 args[0].error = oOptions.error;
4491 }
4492
4493 for (i = 0, j = srcQueue.length; i < j; i++) {
4494 if (srcQueue[i].fired !== true) {
4495 queue.push(srcQueue[i]);
4496 }
4497 }
4498
4499 if (queue.length) {
4500 // sm2._wD(sm + ': Firing ' + queue.length + ' ' + oOptions.type + '() item' + (queue.length === 1 ? '' : 's'));
4501 for (i = 0, j = queue.length; i < j; i++) {
4502 if (queue[i].scope) {
4503 queue[i].method.apply(queue[i].scope, args);
4504 } else {
4505 queue[i].method.apply(this, args);
4506 }
4507 if (!canRetry) {
4508 // useFlashBlock and SWF timeout case doesn't count here.
4509 queue[i].fired = true;
4510 }
4511 }
4512 }
4513
4514 return true;
4515
4516 };
4517
4518 initUserOnload = function() {
4519
4520 window.setTimeout(function() {
4521
4522 if (sm2.useFlashBlock) {
4523 flashBlockHandler();
4524 }
4525
4526 processOnEvents();
4527
4528 // call user-defined "onload", scoped to window
4529
4530 if (typeof sm2.onload === 'function') {
4531 _wDS('onload', 1);
4532 sm2.onload.apply(window);
4533 _wDS('onloadOK', 1);
4534 }
4535
4536 if (sm2.waitForWindowLoad) {
4537 event.add(window, 'load', initUserOnload);
4538 }
4539
4540 },1);
4541
4542 };
4543
4544 detectFlash = function() {
4545
4546 // 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
4547
4548 if (hasFlash !== _undefined) {
4549 // this work has already been done.
4550 return hasFlash;
4551 }
4552
4553 var hasPlugin = false, n = navigator, nP = n.plugins, obj, type, types, AX = window.ActiveXObject;
4554
4555 if (nP && nP.length) {
4556 type = 'application/x-shockwave-flash';
4557 types = n.mimeTypes;
4558 if (types && types[type] && types[type].enabledPlugin && types[type].enabledPlugin.description) {
4559 hasPlugin = true;
4560 }
4561 } else if (AX !== _undefined && !ua.match(/MSAppHost/i)) {
4562 // Windows 8 Store Apps (MSAppHost) are weird (compatibility?) and won't complain here, but will barf if Flash/ActiveX object is appended to the DOM.
4563 try {
4564 obj = new AX('ShockwaveFlash.ShockwaveFlash');
4565 } catch(e) {
4566 // oh well
4567 }
4568 hasPlugin = (!!obj);
4569 // cleanup, because it is ActiveX after all
4570 obj = null;
4571 }
4572
4573 hasFlash = hasPlugin;
4574
4575 return hasPlugin;
4576
4577 };
4578
4579 featureCheck = function() {
4580
4581 var needsFlash,
4582 item,
4583 result = true,
4584 formats = sm2.audioFormats,
4585 // iPhone <= 3.1 has broken HTML5 audio(), but firmware 3.2 (original iPad) + iOS4 works.
4586 isSpecial = (is_iDevice && !!(ua.match(/os (1|2|3_0|3_1)/i)));
4587
4588 if (isSpecial) {
4589
4590 // has Audio(), but is broken; let it load links directly.
4591 sm2.hasHTML5 = false;
4592
4593 // ignore flash case, however
4594 sm2.html5Only = true;
4595
4596 if (sm2.oMC) {
4597 sm2.oMC.style.display = 'none';
4598 }
4599
4600 result = false;
4601
4602 } else {
4603
4604 if (sm2.useHTML5Audio) {
4605
4606 if (!sm2.html5 || !sm2.html5.canPlayType) {
4607 sm2._wD('SoundManager: No HTML5 Audio() support detected.');
4608 sm2.hasHTML5 = false;
4609 }
4610
4611 // <d>
4612 if (isBadSafari) {
4613 sm2._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);
4614 }
4615 // </d>
4616
4617 }
4618
4619 }
4620
4621 if (sm2.useHTML5Audio && sm2.hasHTML5) {
4622
4623 for (item in formats) {
4624 if (formats.hasOwnProperty(item)) {
4625 if ((formats[item].required && !sm2.html5.canPlayType(formats[item].type)) || (sm2.preferFlash && (sm2.flash[item] || sm2.flash[formats[item].type]))) {
4626 // flash may be required, or preferred for this format
4627 needsFlash = true;
4628 }
4629 }
4630 }
4631
4632 }
4633
4634 // sanity check...
4635 if (sm2.ignoreFlash) {
4636 needsFlash = false;
4637 }
4638
4639 sm2.html5Only = (sm2.hasHTML5 && sm2.useHTML5Audio && !needsFlash);
4640
4641 return (!sm2.html5Only);
4642
4643 };
4644
4645 parseURL = function(url) {
4646
4647 /**
4648 * Internal: Finds and returns the first playable URL (or failing that, the first URL.)
4649 * @param {string or array} url A single URL string, OR, an array of URL strings or {url:'/path/to/resource', type:'audio/mp3'} objects.
4650 */
4651
4652 var i, j, urlResult = 0, result;
4653
4654 if (url instanceof Array) {
4655
4656 // find the first good one
4657 for (i=0, j=url.length; i<j; i++) {
4658
4659 if (url[i] instanceof Object) {
4660 // MIME check
4661 if (sm2.canPlayMIME(url[i].type)) {
4662 urlResult = i;
4663 break;
4664 }
4665
4666 } else if (sm2.canPlayURL(url[i])) {
4667 // URL string check
4668 urlResult = i;
4669 break;
4670 }
4671
4672 }
4673
4674 // normalize to string
4675 if (url[urlResult].url) {
4676 url[urlResult] = url[urlResult].url;
4677 }
4678
4679 result = url[urlResult];
4680
4681 } else {
4682
4683 // single URL case
4684 result = url;
4685
4686 }
4687
4688 return result;
4689
4690 };
4691
4692
4693 startTimer = function(oSound) {
4694
4695 /**
4696 * attach a timer to this sound, and start an interval if needed
4697 */
4698
4699 if (!oSound._hasTimer) {
4700
4701 oSound._hasTimer = true;
4702
4703 if (!mobileHTML5 && sm2.html5PollingInterval) {
4704
4705 if (h5IntervalTimer === null && h5TimerCount === 0) {
4706
4707 h5IntervalTimer = setInterval(timerExecute, sm2.html5PollingInterval);
4708
4709 }
4710
4711 h5TimerCount++;
4712
4713 }
4714
4715 }
4716
4717 };
4718
4719 stopTimer = function(oSound) {
4720
4721 /**
4722 * detach a timer
4723 */
4724
4725 if (oSound._hasTimer) {
4726
4727 oSound._hasTimer = false;
4728
4729 if (!mobileHTML5 && sm2.html5PollingInterval) {
4730
4731 // interval will stop itself at next execution.
4732
4733 h5TimerCount--;
4734
4735 }
4736
4737 }
4738
4739 };
4740
4741 timerExecute = function() {
4742
4743 /**
4744 * manual polling for HTML5 progress events, ie., whileplaying() (can achieve greater precision than conservative default HTML5 interval)
4745 */
4746
4747 var i;
4748
4749 if (h5IntervalTimer !== null && !h5TimerCount) {
4750
4751 // no active timers, stop polling interval.
4752
4753 clearInterval(h5IntervalTimer);
4754
4755 h5IntervalTimer = null;
4756
4757 return false;
4758
4759 }
4760
4761 // check all HTML5 sounds with timers
4762
4763 for (i = sm2.soundIDs.length-1; i >= 0; i--) {
4764
4765 if (sm2.sounds[sm2.soundIDs[i]].isHTML5 && sm2.sounds[sm2.soundIDs[i]]._hasTimer) {
4766
4767 sm2.sounds[sm2.soundIDs[i]]._onTimer();
4768
4769 }
4770
4771 }
4772
4773 };
4774
4775 catchError = function(options) {
4776
4777 options = (options !== _undefined ? options : {});
4778
4779 if (typeof sm2.onerror === 'function') {
4780 sm2.onerror.apply(window, [{type:(options.type !== _undefined ? options.type : null)}]);
4781 }
4782
4783 if (options.fatal !== _undefined && options.fatal) {
4784 sm2.disable();
4785 }
4786
4787 };
4788
4789 badSafariFix = function() {
4790
4791 // special case: "bad" Safari (OS X 10.3 - 10.7) must fall back to flash for MP3/MP4
4792 if (!isBadSafari || !detectFlash()) {
4793 // doesn't apply
4794 return false;
4795 }
4796
4797 var aF = sm2.audioFormats, i, item;
4798
4799 for (item in aF) {
4800 if (aF.hasOwnProperty(item)) {
4801 if (item === 'mp3' || item === 'mp4') {
4802 sm2._wD(sm + ': Using flash fallback for ' + item + ' format');
4803 sm2.html5[item] = false;
4804 // assign result to related formats, too
4805 if (aF[item] && aF[item].related) {
4806 for (i = aF[item].related.length-1; i >= 0; i--) {
4807 sm2.html5[aF[item].related[i]] = false;
4808 }
4809 }
4810 }
4811 }
4812 }
4813
4814 };
4815
4816 /**
4817 * Pseudo-private flash/ExternalInterface methods
4818 * ----------------------------------------------
4819 */
4820
4821 this._setSandboxType = function(sandboxType) {
4822
4823 // <d>
4824 var sb = sm2.sandbox;
4825
4826 sb.type = sandboxType;
4827 sb.description = sb.types[(sb.types[sandboxType] !== _undefined?sandboxType:'unknown')];
4828
4829 if (sb.type === 'localWithFile') {
4830
4831 sb.noRemote = true;
4832 sb.noLocal = false;
4833 _wDS('secNote', 2);
4834
4835 } else if (sb.type === 'localWithNetwork') {
4836
4837 sb.noRemote = false;
4838 sb.noLocal = true;
4839
4840 } else if (sb.type === 'localTrusted') {
4841
4842 sb.noRemote = false;
4843 sb.noLocal = false;
4844
4845 }
4846 // </d>
4847
4848 };
4849
4850 this._externalInterfaceOK = function(flashDate, swfVersion) {
4851
4852 // flash callback confirming flash loaded, EI working etc.
4853 // flashDate = approx. timing/delay info for JS/flash bridge
4854 // swfVersion: SWF build string
4855
4856 if (sm2.swfLoaded) {
4857 return false;
4858 }
4859
4860 var e;
4861
4862 debugTS('swf', true);
4863 debugTS('flashtojs', true);
4864 sm2.swfLoaded = true;
4865 tryInitOnFocus = false;
4866
4867 if (isBadSafari) {
4868 badSafariFix();
4869 }
4870
4871 // complain if JS + SWF build/version strings don't match, excluding +DEV builds
4872 // <d>
4873 if (!swfVersion || swfVersion.replace(/\+dev/i,'') !== sm2.versionNumber.replace(/\+dev/i, '')) {
4874
4875 e = sm + ': Fatal: JavaScript file build "' + sm2.versionNumber + '" does not match Flash SWF build "' + swfVersion + '" at ' + sm2.url + '. Ensure both are up-to-date.';
4876
4877 // escape flash -> JS stack so this error fires in window.
4878 setTimeout(function versionMismatch() {
4879 throw new Error(e);
4880 }, 0);
4881
4882 // exit, init will fail with timeout
4883 return false;
4884
4885 }
4886 // </d>
4887
4888 // slight delay before init
4889 setTimeout(init, isIE ? 100 : 1);
4890
4891 };
4892
4893 /**
4894 * Private initialization helpers
4895 * ------------------------------
4896 */
4897
4898 createMovie = function(smID, smURL) {
4899
4900 if (didAppend && appendSuccess) {
4901 // ignore if already succeeded
4902 return false;
4903 }
4904
4905 function initMsg() {
4906
4907 // <d>
4908
4909 var options = [], title, str = [], delimiter = ' + ';
4910
4911 title = 'SoundManager ' + sm2.version + (!sm2.html5Only && sm2.useHTML5Audio ? (sm2.hasHTML5 ? ' + HTML5 audio' : ', no HTML5 audio support') : '');
4912
4913 if (!sm2.html5Only) {
4914
4915 if (sm2.preferFlash) {
4916 options.push('preferFlash');
4917 }
4918
4919 if (sm2.useHighPerformance) {
4920 options.push('useHighPerformance');
4921 }
4922
4923 if (sm2.flashPollingInterval) {
4924 options.push('flashPollingInterval (' + sm2.flashPollingInterval + 'ms)');
4925 }
4926
4927 if (sm2.html5PollingInterval) {
4928 options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)');
4929 }
4930
4931 if (sm2.wmode) {
4932 options.push('wmode (' + sm2.wmode + ')');
4933 }
4934
4935 if (sm2.debugFlash) {
4936 options.push('debugFlash');
4937 }
4938
4939 if (sm2.useFlashBlock) {
4940 options.push('flashBlock');
4941 }
4942
4943 } else {
4944
4945 if (sm2.html5PollingInterval) {
4946 options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)');
4947 }
4948
4949 }
4950
4951 if (options.length) {
4952 str = str.concat([options.join(delimiter)]);
4953 }
4954
4955 sm2._wD(title + (str.length ? delimiter + str.join(', ') : ''), 1);
4956
4957 showSupport();
4958
4959 // </d>
4960
4961 }
4962
4963 if (sm2.html5Only) {
4964
4965 // 100% HTML5 mode
4966 setVersionInfo();
4967
4968 initMsg();
4969 sm2.oMC = id(sm2.movieID);
4970 init();
4971
4972 // prevent multiple init attempts
4973 didAppend = true;
4974
4975 appendSuccess = true;
4976
4977 return false;
4978
4979 }
4980
4981 // flash path
4982 var remoteURL = (smURL || sm2.url),
4983 localURL = (sm2.altURL || remoteURL),
4984 swfTitle = 'JS/Flash audio component (SoundManager 2)',
4985 oTarget = getDocument(),
4986 extraClass = getSWFCSS(),
4987 isRTL = null,
4988 html = doc.getElementsByTagName('html')[0],
4989 oEmbed, oMovie, tmp, movieHTML, oEl, s, x, sClass;
4990
4991 isRTL = (html && html.dir && html.dir.match(/rtl/i));
4992 smID = (smID === _undefined?sm2.id:smID);
4993
4994 function param(name, value) {
4995 return '<param name="'+name+'" value="'+value+'" />';
4996 }
4997
4998 // safety check for legacy (change to Flash 9 URL)
4999 setVersionInfo();
5000 sm2.url = normalizeMovieURL(overHTTP?remoteURL:localURL);
5001 smURL = sm2.url;
5002
5003 sm2.wmode = (!sm2.wmode && sm2.useHighPerformance ? 'transparent' : sm2.wmode);
5004
5005 if (sm2.wmode !== null && (ua.match(/msie 8/i) || (!isIE && !sm2.useHighPerformance)) && navigator.platform.match(/win32|win64/i)) {
5006 /**
5007 * extra-special case: movie doesn't load until scrolled into view when using wmode = anything but 'window' here
5008 * does not apply when using high performance (position:fixed means on-screen), OR infinite flash load timeout
5009 * wmode breaks IE 8 on Vista + Win7 too in some cases, as of January 2011 (?)
5010 */
5011 messages.push(strings.spcWmode);
5012 sm2.wmode = null;
5013 }
5014
5015 oEmbed = {
5016 'name': smID,
5017 'id': smID,
5018 'src': smURL,
5019 'quality': 'high',
5020 'allowScriptAccess': sm2.allowScriptAccess,
5021 'bgcolor': sm2.bgColor,
5022 'pluginspage': http+'www.macromedia.com/go/getflashplayer',
5023 'title': swfTitle,
5024 'type': 'application/x-shockwave-flash',
5025 'wmode': sm2.wmode,
5026 // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
5027 'hasPriority': 'true'
5028 };
5029
5030 if (sm2.debugFlash) {
5031 oEmbed.FlashVars = 'debug=1';
5032 }
5033
5034 if (!sm2.wmode) {
5035 // don't write empty attribute
5036 delete oEmbed.wmode;
5037 }
5038
5039 if (isIE) {
5040
5041 // IE is "special".
5042 oMovie = doc.createElement('div');
5043 movieHTML = [
5044 '<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">',
5045 param('movie', smURL),
5046 param('AllowScriptAccess', sm2.allowScriptAccess),
5047 param('quality', oEmbed.quality),
5048 (sm2.wmode? param('wmode', sm2.wmode): ''),
5049 param('bgcolor', sm2.bgColor),
5050 param('hasPriority', 'true'),
5051 (sm2.debugFlash ? param('FlashVars', oEmbed.FlashVars) : ''),
5052 '</object>'
5053 ].join('');
5054
5055 } else {
5056
5057 oMovie = doc.createElement('embed');
5058 for (tmp in oEmbed) {
5059 if (oEmbed.hasOwnProperty(tmp)) {
5060 oMovie.setAttribute(tmp, oEmbed[tmp]);
5061 }
5062 }
5063
5064 }
5065
5066 initDebug();
5067 extraClass = getSWFCSS();
5068 oTarget = getDocument();
5069
5070 if (oTarget) {
5071
5072 sm2.oMC = (id(sm2.movieID) || doc.createElement('div'));
5073
5074 if (!sm2.oMC.id) {
5075
5076 sm2.oMC.id = sm2.movieID;
5077 sm2.oMC.className = swfCSS.swfDefault + ' ' + extraClass;
5078 s = null;
5079 oEl = null;
5080
5081 if (!sm2.useFlashBlock) {
5082 if (sm2.useHighPerformance) {
5083 // on-screen at all times
5084 s = {
5085 'position': 'fixed',
5086 'width': '8px',
5087 'height': '8px',
5088 // >= 6px for flash to run fast, >= 8px to start up under Firefox/win32 in some cases. odd? yes.
5089 'bottom': '0px',
5090 'left': '0px',
5091 'overflow': 'hidden'
5092 };
5093 } else {
5094 // hide off-screen, lower priority
5095 s = {
5096 'position': 'absolute',
5097 'width': '6px',
5098 'height': '6px',
5099 'top': '-9999px',
5100 'left': '-9999px'
5101 };
5102 if (isRTL) {
5103 s.left = Math.abs(parseInt(s.left,10))+'px';
5104 }
5105 }
5106 }
5107
5108 if (isWebkit) {
5109 // soundcloud-reported render/crash fix, safari 5
5110 sm2.oMC.style.zIndex = 10000;
5111 }
5112
5113 if (!sm2.debugFlash) {
5114 for (x in s) {
5115 if (s.hasOwnProperty(x)) {
5116 sm2.oMC.style[x] = s[x];
5117 }
5118 }
5119 }
5120
5121 try {
5122 if (!isIE) {
5123 sm2.oMC.appendChild(oMovie);
5124 }
5125 oTarget.appendChild(sm2.oMC);
5126 if (isIE) {
5127 oEl = sm2.oMC.appendChild(doc.createElement('div'));
5128 oEl.className = swfCSS.swfBox;
5129 oEl.innerHTML = movieHTML;
5130 }
5131 appendSuccess = true;
5132 } catch(e) {
5133 throw new Error(str('domError')+' \n'+e.toString());
5134 }
5135
5136 } else {
5137
5138 // SM2 container is already in the document (eg. flashblock use case)
5139 sClass = sm2.oMC.className;
5140 sm2.oMC.className = (sClass?sClass+' ':swfCSS.swfDefault) + (extraClass?' '+extraClass:'');
5141 sm2.oMC.appendChild(oMovie);
5142 if (isIE) {
5143 oEl = sm2.oMC.appendChild(doc.createElement('div'));
5144 oEl.className = swfCSS.swfBox;
5145 oEl.innerHTML = movieHTML;
5146 }
5147 appendSuccess = true;
5148
5149 }
5150
5151 }
5152
5153 didAppend = true;
5154 initMsg();
5155 // sm2._wD(sm + ': Trying to load ' + smURL + (!overHTTP && sm2.altURL ? ' (alternate URL)' : ''), 1);
5156
5157 return true;
5158
5159 };
5160
5161 initMovie = function() {
5162
5163 if (sm2.html5Only) {
5164 createMovie();
5165 return false;
5166 }
5167
5168 // attempt to get, or create, movie (may already exist)
5169 if (flash) {
5170 return false;
5171 }
5172
5173 if (!sm2.url) {
5174
5175 /**
5176 * Something isn't right - we've reached init, but the soundManager url property has not been set.
5177 * User has not called setup({url: ...}), or has not set soundManager.url (legacy use case) directly before init time.
5178 * Notify and exit. If user calls setup() with a url: property, init will be restarted as in the deferred loading case.
5179 */
5180
5181 _wDS('noURL');
5182 return false;
5183
5184 }
5185
5186 // inline markup case
5187 flash = sm2.getMovie(sm2.id);
5188
5189 if (!flash) {
5190 if (!oRemoved) {
5191 // try to create
5192 createMovie(sm2.id, sm2.url);
5193 } else {
5194 // try to re-append removed movie after reboot()
5195 if (!isIE) {
5196 sm2.oMC.appendChild(oRemoved);
5197 } else {
5198 sm2.oMC.innerHTML = oRemovedHTML;
5199 }
5200 oRemoved = null;
5201 didAppend = true;
5202 }
5203 flash = sm2.getMovie(sm2.id);
5204 }
5205
5206 if (typeof sm2.oninitmovie === 'function') {
5207 setTimeout(sm2.oninitmovie, 1);
5208 }
5209
5210 // <d>
5211 flushMessages();
5212 // </d>
5213
5214 return true;
5215
5216 };
5217
5218 delayWaitForEI = function() {
5219
5220 setTimeout(waitForEI, 1000);
5221
5222 };
5223
5224 waitForEI = function() {
5225
5226 var p,
5227 loadIncomplete = false;
5228
5229 if (!sm2.url) {
5230 // No SWF url to load (noURL case) - exit for now. Will be retried when url is set.
5231 return false;
5232 }
5233
5234 if (waitingForEI) {
5235 return false;
5236 }
5237
5238 waitingForEI = true;
5239 event.remove(window, 'load', delayWaitForEI);
5240
5241 if (tryInitOnFocus && !isFocused) {
5242 // Safari won't load flash in background tabs, only when focused.
5243 _wDS('waitFocus');
5244 return false;
5245 }
5246
5247 if (!didInit) {
5248 p = sm2.getMoviePercent();
5249 if (p > 0 && p < 100) {
5250 loadIncomplete = true;
5251 }
5252 }
5253
5254 setTimeout(function() {
5255
5256 p = sm2.getMoviePercent();
5257
5258 if (loadIncomplete) {
5259 // special case: if movie *partially* loaded, retry until it's 100% before assuming failure.
5260 waitingForEI = false;
5261 sm2._wD(str('waitSWF'));
5262 window.setTimeout(delayWaitForEI, 1);
5263 return false;
5264 }
5265
5266 // <d>
5267 if (!didInit) {
5268 sm2._wD(sm + ': No Flash response within expected time. Likely causes: ' + (p === 0 ? 'SWF load failed, ':'') + 'Flash blocked or JS-Flash security error.' + (sm2.debugFlash?' ' + str('checkSWF'):''), 2);
5269 if (!overHTTP && p) {
5270 _wDS('localFail', 2);
5271 if (!sm2.debugFlash) {
5272 _wDS('tryDebug', 2);
5273 }
5274 }
5275 if (p === 0) {
5276 // if 0 (not null), probably a 404.
5277 sm2._wD(str('swf404', sm2.url), 1);
5278 }
5279 debugTS('flashtojs', false, ': Timed out' + overHTTP?' (Check flash security or flash blockers)':' (No plugin/missing SWF?)');
5280 }
5281 // </d>
5282
5283 // give up / time-out, depending
5284
5285 if (!didInit && okToDisable) {
5286 if (p === null) {
5287 // SWF failed. Maybe blocked.
5288 if (sm2.useFlashBlock || sm2.flashLoadTimeout === 0) {
5289 if (sm2.useFlashBlock) {
5290 flashBlockHandler();
5291 }
5292 _wDS('waitForever');
5293 } else {
5294 // no custom flash block handling, but SWF has timed out. Will recover if user unblocks / allows SWF load.
5295 _wDS('waitForever');
5296 // fire any regular registered ontimeout() listeners.
5297 processOnEvents({type:'ontimeout', ignoreInit: true});
5298 }
5299 } else {
5300 // flash loaded? Shouldn't be a blocking issue, then.
5301 if (sm2.flashLoadTimeout === 0) {
5302 _wDS('waitForever');
5303 } else {
5304 failSafely(true);
5305 }
5306 }
5307 }
5308
5309 }, sm2.flashLoadTimeout);
5310
5311 };
5312
5313 handleFocus = function() {
5314
5315 function cleanup() {
5316 event.remove(window, 'focus', handleFocus);
5317 }
5318
5319 if (isFocused || !tryInitOnFocus) {
5320 // already focused, or not special Safari background tab case
5321 cleanup();
5322 return true;
5323 }
5324
5325 okToDisable = true;
5326 isFocused = true;
5327 _wDS('gotFocus');
5328
5329 // allow init to restart
5330 waitingForEI = false;
5331
5332 // kick off ExternalInterface timeout, now that the SWF has started
5333 delayWaitForEI();
5334
5335 cleanup();
5336 return true;
5337
5338 };
5339
5340 flushMessages = function() {
5341
5342 // <d>
5343
5344 // SM2 pre-init debug messages
5345 if (messages.length) {
5346 sm2._wD('SoundManager 2: ' + messages.join(' '), 1);
5347 messages = [];
5348 }
5349
5350 // </d>
5351
5352 };
5353
5354 showSupport = function() {
5355
5356 // <d>
5357
5358 flushMessages();
5359
5360 var item, tests = [];
5361
5362 if (sm2.useHTML5Audio && sm2.hasHTML5) {
5363 for (item in sm2.audioFormats) {
5364 if (sm2.audioFormats.hasOwnProperty(item)) {
5365 tests.push(item + ' = ' + sm2.html5[item] + (!sm2.html5[item] && hasFlash && sm2.flash[item] ? ' (using flash)' : (sm2.preferFlash && sm2.flash[item] && hasFlash ? ' (preferring flash)': (!sm2.html5[item] ? ' (' + (sm2.audioFormats[item].required ? 'required, ':'') + 'and no flash support)' : ''))));
5366 }
5367 }
5368 sm2._wD('SoundManager 2 HTML5 support: ' + tests.join(', '), 1);
5369 }
5370
5371 // </d>
5372
5373 };
5374
5375 initComplete = function(bNoDisable) {
5376
5377 if (didInit) {
5378 return false;
5379 }
5380
5381 if (sm2.html5Only) {
5382 // all good.
5383 _wDS('sm2Loaded');
5384 didInit = true;
5385 initUserOnload();
5386 debugTS('onload', true);
5387 return true;
5388 }
5389
5390 var wasTimeout = (sm2.useFlashBlock && sm2.flashLoadTimeout && !sm2.getMoviePercent()),
5391 result = true,
5392 error;
5393
5394 if (!wasTimeout) {
5395 didInit = true;
5396 if (disabled) {
5397 error = {type: (!hasFlash && needsFlash ? 'NO_FLASH' : 'INIT_TIMEOUT')};
5398 }
5399 }
5400
5401 sm2._wD('SoundManager 2 ' + (disabled ? 'failed to load' : 'loaded') + ' (' + (disabled ? 'Flash security/load error' : 'OK') + ')', disabled ? 2: 1);
5402
5403 if (disabled || bNoDisable) {
5404 if (sm2.useFlashBlock && sm2.oMC) {
5405 sm2.oMC.className = getSWFCSS() + ' ' + (sm2.getMoviePercent() === null?swfCSS.swfTimedout:swfCSS.swfError);
5406 }
5407 processOnEvents({type:'ontimeout', error:error, ignoreInit: true});
5408 debugTS('onload', false);
5409 catchError(error);
5410 result = false;
5411 } else {
5412 debugTS('onload', true);
5413 }
5414
5415 if (!disabled) {
5416 if (sm2.waitForWindowLoad && !windowLoaded) {
5417 _wDS('waitOnload');
5418 event.add(window, 'load', initUserOnload);
5419 } else {
5420 // <d>
5421 if (sm2.waitForWindowLoad && windowLoaded) {
5422 _wDS('docLoaded');
5423 }
5424 // </d>
5425 initUserOnload();
5426 }
5427 }
5428
5429 return result;
5430
5431 };
5432
5433 /**
5434 * apply top-level setupOptions object as local properties, eg., this.setupOptions.flashVersion -> this.flashVersion (soundManager.flashVersion)
5435 * this maintains backward compatibility, and allows properties to be defined separately for use by soundManager.setup().
5436 */
5437
5438 setProperties = function() {
5439
5440 var i,
5441 o = sm2.setupOptions;
5442
5443 for (i in o) {
5444
5445 if (o.hasOwnProperty(i)) {
5446
5447 // assign local property if not already defined
5448
5449 if (sm2[i] === _undefined) {
5450
5451 sm2[i] = o[i];
5452
5453 } else if (sm2[i] !== o[i]) {
5454
5455 // legacy support: write manually-assigned property (eg., soundManager.url) back to setupOptions to keep things in sync
5456 sm2.setupOptions[i] = sm2[i];
5457
5458 }
5459
5460 }
5461
5462 }
5463
5464 };
5465
5466
5467 init = function() {
5468
5469 // called after onload()
5470
5471 if (didInit) {
5472 _wDS('didInit');
5473 return false;
5474 }
5475
5476 function cleanup() {
5477 event.remove(window, 'load', sm2.beginDelayedInit);
5478 }
5479
5480 if (sm2.html5Only) {
5481 if (!didInit) {
5482 // we don't need no steenking flash!
5483 cleanup();
5484 sm2.enabled = true;
5485 initComplete();
5486 }
5487 return true;
5488 }
5489
5490 // flash path
5491 initMovie();
5492
5493 try {
5494
5495 // attempt to talk to Flash
5496 flash._externalInterfaceTest(false);
5497
5498 // apply user-specified polling interval, OR, if "high performance" set, faster vs. default polling
5499 // (determines frequency of whileloading/whileplaying callbacks, effectively driving UI framerates)
5500 setPolling(true, (sm2.flashPollingInterval || (sm2.useHighPerformance ? 10 : 50)));
5501
5502 if (!sm2.debugMode) {
5503 // stop the SWF from making debug output calls to JS
5504 flash._disableDebug();
5505 }
5506
5507 sm2.enabled = true;
5508 debugTS('jstoflash', true);
5509
5510 if (!sm2.html5Only) {
5511 // prevent browser from showing cached page state (or rather, restoring "suspended" page state) via back button, because flash may be dead
5512 // http://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
5513 event.add(window, 'unload', doNothing);
5514 }
5515
5516 } catch(e) {
5517
5518 sm2._wD('js/flash exception: ' + e.toString());
5519 debugTS('jstoflash', false);
5520 catchError({type:'JS_TO_FLASH_EXCEPTION', fatal:true});
5521 // don't disable, for reboot()
5522 failSafely(true);
5523 initComplete();
5524
5525 return false;
5526
5527 }
5528
5529 initComplete();
5530
5531 // disconnect events
5532 cleanup();
5533
5534 return true;
5535
5536 };
5537
5538 domContentLoaded = function() {
5539
5540 if (didDCLoaded) {
5541 return false;
5542 }
5543
5544 didDCLoaded = true;
5545
5546 // assign top-level soundManager properties eg. soundManager.url
5547 setProperties();
5548
5549 initDebug();
5550
5551 /**
5552 * Temporary feature: allow force of HTML5 via URL params: sm2-usehtml5audio=0 or 1
5553 * Ditto for sm2-preferFlash, too.
5554 */
5555 // <d>
5556 (function(){
5557
5558 var a = 'sm2-usehtml5audio=',
5559 a2 = 'sm2-preferflash=',
5560 b = null,
5561 b2 = null,
5562 hasCon = (window.console !== _undefined && typeof console.log === 'function'),
5563 l = wl.toLowerCase();
5564
5565 if (l.indexOf(a) !== -1) {
5566 b = (l.charAt(l.indexOf(a)+a.length) === '1');
5567 if (hasCon) {
5568 console.log((b?'Enabling ':'Disabling ')+'useHTML5Audio via URL parameter');
5569 }
5570 sm2.setup({
5571 'useHTML5Audio': b
5572 });
5573 }
5574
5575 if (l.indexOf(a2) !== -1) {
5576 b2 = (l.charAt(l.indexOf(a2)+a2.length) === '1');
5577 if (hasCon) {
5578 console.log((b2?'Enabling ':'Disabling ')+'preferFlash via URL parameter');
5579 }
5580 sm2.setup({
5581 'preferFlash': b2
5582 });
5583 }
5584
5585 }());
5586 // </d>
5587
5588 if (!hasFlash && sm2.hasHTML5) {
5589 sm2._wD('SoundManager: No Flash detected' + (!sm2.useHTML5Audio ? ', enabling HTML5.' : '. Trying HTML5-only mode.'), 1);
5590 sm2.setup({
5591 'useHTML5Audio': true,
5592 // make sure we aren't preferring flash, either
5593 // TODO: preferFlash should not matter if flash is not installed. Currently, stuff breaks without the below tweak.
5594 'preferFlash': false
5595 });
5596 }
5597
5598 testHTML5();
5599 sm2.html5.usingFlash = featureCheck();
5600 needsFlash = sm2.html5.usingFlash;
5601
5602 if (!hasFlash && needsFlash) {
5603 messages.push(strings.needFlash);
5604 // TODO: Fatal here vs. timeout approach, etc.
5605 // hack: fail sooner.
5606 sm2.setup({
5607 'flashLoadTimeout': 1
5608 });
5609 }
5610
5611 if (doc.removeEventListener) {
5612 doc.removeEventListener('DOMContentLoaded', domContentLoaded, false);
5613 }
5614
5615 initMovie();
5616
5617 return true;
5618
5619 };
5620
5621 domContentLoadedIE = function() {
5622
5623 if (doc.readyState === 'complete') {
5624 domContentLoaded();
5625 doc.detachEvent('onreadystatechange', domContentLoadedIE);
5626 }
5627
5628 return true;
5629
5630 };
5631
5632 winOnLoad = function() {
5633
5634 // catch edge case of initComplete() firing after window.load()
5635 windowLoaded = true;
5636 event.remove(window, 'load', winOnLoad);
5637
5638 };
5639
5640 /**
5641 * miscellaneous run-time, pre-init stuff
5642 */
5643
5644 preInit = function() {
5645
5646 if (mobileHTML5) {
5647
5648 // prefer HTML5 for mobile + tablet-like devices, probably more reliable vs. flash at this point.
5649
5650 // <d>
5651 if (!sm2.setupOptions.useHTML5Audio || sm2.setupOptions.preferFlash) {
5652 // notify that defaults are being changed.
5653 messages.push(strings.mobileUA);
5654 }
5655 // </d>
5656
5657 sm2.setupOptions.useHTML5Audio = true;
5658 sm2.setupOptions.preferFlash = false;
5659
5660 if (is_iDevice || (isAndroid && !ua.match(/android\s2\.3/i))) {
5661 // iOS and Android devices tend to work better with a single audio instance, specifically for chained playback of sounds in sequence.
5662 // common use case: exiting sound onfinish() -> createSound() -> play()
5663 // <d>
5664 messages.push(strings.globalHTML5);
5665 // </d>
5666 if (is_iDevice) {
5667 sm2.ignoreFlash = true;
5668 }
5669 useGlobalHTML5Audio = true;
5670 }
5671
5672 }
5673
5674 };
5675
5676 preInit();
5677
5678 // sniff up-front
5679 detectFlash();
5680
5681 // focus and window load, init (primarily flash-driven)
5682 event.add(window, 'focus', handleFocus);
5683 event.add(window, 'load', delayWaitForEI);
5684 event.add(window, 'load', winOnLoad);
5685
5686 if (doc.addEventListener) {
5687
5688 doc.addEventListener('DOMContentLoaded', domContentLoaded, false);
5689
5690 } else if (doc.attachEvent) {
5691
5692 doc.attachEvent('onreadystatechange', domContentLoadedIE);
5693
5694 } else {
5695
5696 // no add/attachevent support - safe to assume no JS -> Flash either
5697 debugTS('onload', false);
5698 catchError({type:'NO_DOM2_EVENTS', fatal:true});
5699
5700 }
5701
5702 } // SoundManager()
5703
5704 // SM2_DEFER details: http://www.schillmania.com/projects/soundmanager2/doc/getstarted/#lazy-loading
5705
5706 if (window.SM2_DEFER === undefined || !SM2_DEFER) {
5707 soundManager = new SoundManager();
5708 }
5709
5710 /**
5711 * SoundManager public interfaces
5712 * ------------------------------
5713 */
5714
5715 window.SoundManager = SoundManager; // constructor
5716 window.soundManager = soundManager; // public API, flash callbacks etc.
5717
5718 }(window));