· 8 years ago · Dec 05, 2017, 03:12 AM
1<script type="text/javascript">/** @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.20170601
12 */
13
14/**
15 * About this file
16 * -------------------------------------------------------------------------------------
17 * This is the fully-commented source version of the SoundManager 2 API,
18 * recommended for use during development and testing.
19 *
20 * See soundmanager2-nodebug-jsmin.js for an optimized build (~11KB with gzip.)
21 * http://schillmania.com/projects/soundmanager2/doc/getstarted/#basic-inclusion
22 * Alternately, serve this file with gzip for 75% compression savings (~30KB over HTTP.)
23 *
24 * You may notice <d> and </d> comments in this source; these are delimiters for
25 * debug blocks which are removed in the -nodebug builds, further optimizing code size.
26 *
27 * Also, as you may note: Whoa, reliable cross-platform/device audio support is hard! ;)
28 */
29
30(function SM2(window, _undefined) {
31
32/* global Audio, document, window, navigator, define, module, SM2_DEFER, opera, setTimeout, setInterval, clearTimeout, sm2Debugger */
33
34'use strict';
35
36if (!window || !window.document) {
37
38 // Don't cross the [environment] streams. SM2 expects to be running in a browser, not under node.js etc.
39 // Additionally, if a browser somehow manages to fail this test, as Egon said: "It would be bad."
40
41 throw new Error('SoundManager requires a browser with window and document objects.');
42
43}
44
45var soundManager = null;
46
47/**
48 * The SoundManager constructor.
49 *
50 * @constructor
51 * @param {string} smURL Optional: Path to SWF files
52 * @param {string} smID Optional: The ID to use for the SWF container element
53 * @this {SoundManager}
54 * @return {SoundManager} The new SoundManager instance
55 */
56
57function SoundManager(smURL, smID) {
58
59 /**
60 * soundManager configuration options list
61 * defines top-level configuration properties to be applied to the soundManager instance (eg. soundManager.flashVersion)
62 * to set these properties, use the setup() method - eg., soundManager.setup({url: '/swf/', flashVersion: 9})
63 */
64
65 this.setupOptions = {
66
67 url: (smURL || null), // path (directory) where SoundManager 2 SWFs exist, eg., /path/to/swfs/
68 flashVersion: 8, // flash build to use (8 or 9.) Some API features require 9.
69 debugMode: true, // enable debugging output (console.log() with HTML fallback)
70 debugFlash: false, // enable debugging output inside SWF, troubleshoot Flash/browser issues
71 useConsole: true, // use console.log() if available (otherwise, writes to #soundmanager-debug element)
72 consoleOnly: true, // if console is being used, do not create/write to #soundmanager-debug
73 waitForWindowLoad: false, // force SM2 to wait for window.onload() before trying to call soundManager.onload()
74 bgColor: '#ffffff', // SWF background color. N/A when wmode = 'transparent'
75 useHighPerformance: false, // position:fixed flash movie can help increase js/flash speed, minimize lag
76 flashPollingInterval: null, // msec affecting whileplaying/loading callback frequency. If null, default of 50 msec is used.
77 html5PollingInterval: null, // msec affecting whileplaying() for HTML5 audio, excluding mobile devices. If null, native HTML5 update events are used.
78 flashLoadTimeout: 1000, // msec to wait for flash movie to load before failing (0 = infinity)
79 wmode: null, // flash rendering mode - null, 'transparent', or 'opaque' (last two allow z-index to work)
80 allowScriptAccess: 'always', // for scripting the SWF (object/embed property), 'always' or 'sameDomain'
81 useFlashBlock: false, // *requires flashblock.css, see demos* - allow recovery from flash blockers. Wait indefinitely and apply timeout CSS to SWF, if applicable.
82 useHTML5Audio: true, // use HTML5 Audio() where API is supported (most Safari, Chrome versions), Firefox (MP3/MP4 support varies.) Ideally, transparent vs. Flash API where possible.
83 forceUseGlobalHTML5Audio: false, // if true, a single Audio() object is used for all sounds - and only one can play at a time.
84 ignoreMobileRestrictions: false, // if true, SM2 will not apply global HTML5 audio rules to mobile UAs. iOS > 7 and WebViews may allow multiple Audio() instances.
85 html5Test: /^(probably|maybe)$/i, // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative.
86 preferFlash: false, // overrides useHTML5audio, will use Flash for MP3/MP4/AAC if present. Potential option if HTML5 playback with these formats is quirky.
87 noSWFCache: false, // if true, appends ?ts={date} to break aggressive SWF caching.
88 idPrefix: 'sound' // if an id is not provided to createSound(), this prefix is used for generated IDs - 'sound0', 'sound1' etc.
89
90 };
91
92 this.defaultOptions = {
93
94 /**
95 * the default configuration for sound objects made with createSound() and related methods
96 * eg., volume, auto-load behaviour and so forth
97 */
98
99 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)
100 autoPlay: false, // enable playing of file as soon as possible (much faster if "stream" is true)
101 from: null, // position to start playback within a sound (msec), default = beginning
102 loops: 1, // how many times to repeat the sound (position will wrap around to 0, setPosition() will break out of loop when >0)
103 onid3: null, // callback function for "ID3 data is added/available"
104 onerror: null, // callback function for "load failed" (or, playback/network/decode error under HTML5.)
105 onload: null, // callback function for "load finished"
106 whileloading: null, // callback function for "download progress update" (X of Y bytes received)
107 onplay: null, // callback for "play" start
108 onpause: null, // callback for "pause"
109 onresume: null, // callback for "resume" (pause toggle)
110 whileplaying: null, // callback during play (position update)
111 onposition: null, // object containing times and function callbacks for positions of interest
112 onstop: null, // callback for "user stop"
113 onfinish: null, // callback function for "sound finished playing"
114 multiShot: true, // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
115 multiShotEvents: false, // fire multiple sound events (currently onfinish() only) when multiShot is enabled
116 position: null, // offset (milliseconds) to seek to within loaded sound data.
117 pan: 0, // "pan" settings, left-to-right, -100 to 100
118 playbackRate: 1, // rate at which to play the sound (HTML5-only)
119 stream: true, // allows playing before entire file has loaded (recommended)
120 to: null, // position to end playback within a sound (msec), default = end
121 type: null, // MIME-like hint for file pattern / canPlay() tests, eg. audio/mp3
122 usePolicyFile: false, // enable crossdomain.xml request for audio on remote domains (for ID3/waveform access)
123 volume: 100 // self-explanatory. 0-100, the latter being the max.
124
125 };
126
127 this.flash9Options = {
128
129 /**
130 * flash 9-only options,
131 * merged into defaultOptions if flash 9 is being used
132 */
133
134 onfailure: null, // callback function for when playing fails (Flash 9, MovieStar + RTMP-only)
135 isMovieStar: null, // "MovieStar" MPEG4 audio mode. Null (default) = auto detect MP4, AAC etc. based on URL. true = force on, ignore URL
136 usePeakData: false, // enable left/right channel peak (level) data
137 useWaveformData: false, // enable sound spectrum (raw waveform data) - NOTE: May increase CPU load.
138 useEQData: false, // enable sound EQ (frequency spectrum data) - NOTE: May increase CPU load.
139 onbufferchange: null, // callback for "isBuffering" property change
140 ondataerror: null // callback for waveform/eq data access error (flash playing audio in other tabs/domains)
141
142 };
143
144 this.movieStarOptions = {
145
146 /**
147 * flash 9.0r115+ MPEG4 audio options,
148 * merged into defaultOptions if flash 9+movieStar mode is enabled
149 */
150
151 bufferTime: 3, // seconds of data to buffer before playback begins (null = flash default of 0.1 seconds - if AAC playback is gappy, try increasing.)
152 serverURL: null, // rtmp: FMS or FMIS server to connect to, required when requesting media via RTMP or one of its variants
153 onconnect: null, // rtmp: callback for connection to flash media server
154 duration: null // rtmp: song duration (msec)
155
156 };
157
158 this.audioFormats = {
159
160 /**
161 * determines HTML5 support + flash requirements.
162 * if no support (via flash and/or HTML5) for a "required" format, SM2 will fail to start.
163 * flash fallback is used for MP3 or MP4 if HTML5 can't play it (or if preferFlash = true)
164 */
165
166 mp3: {
167 type: ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'],
168 required: true
169 },
170
171 mp4: {
172 related: ['aac', 'm4a', 'm4b'], // additional formats under the MP4 container
173 type: ['audio/mp4; codecs="mp4a.40.2"', 'audio/aac', 'audio/x-m4a', 'audio/MP4A-LATM', 'audio/mpeg4-generic'],
174 required: false
175 },
176
177 ogg: {
178 type: ['audio/ogg; codecs=vorbis'],
179 required: false
180 },
181
182 opus: {
183 type: ['audio/ogg; codecs=opus', 'audio/opus'],
184 required: false
185 },
186
187 wav: {
188 type: ['audio/wav; codecs="1"', 'audio/wav', 'audio/wave', 'audio/x-wav'],
189 required: false
190 },
191
192 flac: {
193 type: ['audio/flac'],
194 required: false
195 }
196
197 };
198
199 // HTML attributes (id + class names) for the SWF container
200
201 this.movieID = 'sm2-container';
202 this.id = (smID || 'sm2movie');
203
204 this.debugID = 'soundmanager-debug';
205 this.debugURLParam = /([#?&])debug=1/i;
206
207 // dynamic attributes
208
209 this.versionNumber = 'V2.97a.20170601';
210 this.version = null;
211 this.movieURL = null;
212 this.altURL = null;
213 this.swfLoaded = false;
214 this.enabled = false;
215 this.oMC = null;
216 this.sounds = {};
217 this.soundIDs = [];
218 this.muted = false;
219 this.didFlashBlock = false;
220 this.filePattern = null;
221
222 this.filePatterns = {
223 flash8: /\.mp3(\?.*)?$/i,
224 flash9: /\.mp3(\?.*)?$/i
225 };
226
227 // support indicators, set at init
228
229 this.features = {
230 buffering: false,
231 peakData: false,
232 waveformData: false,
233 eqData: false,
234 movieStar: false
235 };
236
237 // flash sandbox info, used primarily in troubleshooting
238
239 this.sandbox = {
240 // <d>
241 type: null,
242 types: {
243 remote: 'remote (domain-based) rules',
244 localWithFile: 'local with file access (no internet access)',
245 localWithNetwork: 'local with network (internet access only, no local access)',
246 localTrusted: 'local, trusted (local+internet access)'
247 },
248 description: null,
249 noRemote: null,
250 noLocal: null
251 // </d>
252 };
253
254 /**
255 * format support (html5/flash)
256 * stores canPlayType() results based on audioFormats.
257 * eg. { mp3: boolean, mp4: boolean }
258 * treat as read-only.
259 */
260
261 this.html5 = {
262 usingFlash: null // set if/when flash fallback is needed
263 };
264
265 // file type support hash
266 this.flash = {};
267
268 // determined at init time
269 this.html5Only = false;
270
271 // used for special cases (eg. iPad/iPhone/palm OS?)
272 this.ignoreFlash = false;
273
274 /**
275 * a few private internals (OK, a lot. :D)
276 */
277
278 var SMSound,
279 sm2 = this, globalHTML5Audio = null, flash = null, sm = 'soundManager', smc = sm + ': ', h5 = 'HTML5::', id, ua = navigator.userAgent, wl = window.location.href.toString(), doc = document, doNothing, setProperties, init, fV, on_queue = [], debugOpen = true, debugTS, didAppend = false, appendSuccess = false, didInit = false, disabled = false, windowLoaded = false, _wDS, wdCount = 0, initComplete, mixin, assign, extraOptions, addOnEvent, processOnEvents, initUserOnload, delayWaitForEI, waitForEI, rebootIntoHTML5, setVersionInfo, handleFocus, strings, initMovie, domContentLoaded, winOnLoad, didDCLoaded, getDocument, createMovie, catchError, setPolling, initDebug, debugLevels = ['log', 'info', 'warn', 'error'], defaultFlashVersion = 8, disableObject, failSafely, normalizeMovieURL, oRemoved = null, oRemovedHTML = null, str, flashBlockHandler, getSWFCSS, swfCSS, toggleDebug, loopFix, policyFix, complain, idCheck, waitingForEI = false, initPending = false, startTimer, stopTimer, timerExecute, h5TimerCount = 0, h5IntervalTimer = null, parseURL, messages = [],
280 canIgnoreFlash, needsFlash = null, featureCheck, html5OK, html5CanPlay, html5ErrorCodes, html5Ext, html5Unload, domContentLoadedIE, testHTML5, event, slice = Array.prototype.slice, useGlobalHTML5Audio = false, lastGlobalHTML5URL, hasFlash, detectFlash, badSafariFix, html5_events, showSupport, flushMessages, wrapCallback, idCounter = 0, didSetup, msecScale = 1000,
281 is_iDevice = ua.match(/(ipad|iphone|ipod)/i), isAndroid = ua.match(/android/i), isIE = ua.match(/msie|trident/i),
282 isWebkit = ua.match(/webkit/i),
283 isSafari = (ua.match(/safari/i) && !ua.match(/chrome/i)),
284 isOpera = (ua.match(/opera/i)),
285 mobileHTML5 = (ua.match(/(mobile|pre\/|xoom)/i) || is_iDevice || isAndroid),
286 isBadSafari = (!wl.match(/usehtml5audio/i) && !wl.match(/sm2-ignorebadua/i) && isSafari && !ua.match(/silk/i) && ua.match(/OS\sX\s10_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
287 hasConsole = (window.console !== _undefined && console.log !== _undefined),
288 isFocused = (doc.hasFocus !== _undefined ? doc.hasFocus() : null),
289 tryInitOnFocus = (isSafari && (doc.hasFocus === _undefined || !doc.hasFocus())),
290 okToDisable = !tryInitOnFocus,
291 flashMIME = /(mp3|mp4|mpa|m4a|m4b)/i,
292 emptyURL = 'about:blank', // safe URL to unload, or load nothing from (flash 8 + most HTML5 UAs)
293 emptyWAV = 'data:audio/wave;base64,/UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAD//w==', // tiny WAV for HTML5 unloading
294 overHTTP = (doc.location ? doc.location.protocol.match(/http/i) : null),
295 http = (!overHTTP ? '//' : ''),
296 // mp3, mp4, aac etc.
297 netStreamMimeTypes = /^\s*audio\/(?:x-)?(?:mpeg4|aac|flv|mov|mp4|m4v|m4a|m4b|mp4v|3gp|3g2)\s*(?:$|;)/i,
298 // Flash v9.0r115+ "moviestar" formats
299 netStreamTypes = ['mpeg4', 'aac', 'flv', 'mov', 'mp4', 'm4v', 'f4v', 'm4a', 'm4b', 'mp4v', '3gp', '3g2'],
300 netStreamPattern = new RegExp('\\.(' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i');
301
302 this.mimePattern = /^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i; // default mp3 set
303
304 // use altURL if not "online"
305 this.useAltURL = !overHTTP;
306
307 swfCSS = {
308 swfBox: 'sm2-object-box',
309 swfDefault: 'movieContainer',
310 swfError: 'swf_error', // SWF loaded, but SM2 couldn't start (other error)
311 swfTimedout: 'swf_timedout',
312 swfLoaded: 'swf_loaded',
313 swfUnblocked: 'swf_unblocked', // or loaded OK
314 sm2Debug: 'sm2_debug',
315 highPerf: 'high_performance',
316 flashDebug: 'flash_debug'
317 };
318
319 /**
320 * HTML5 error codes, per W3C
321 * Error code 1, MEDIA_ERR_ABORTED: Client aborted download at user's request.
322 * Error code 2, MEDIA_ERR_NETWORK: A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable.
323 * Error code 3, MEDIA_ERR_DECODE: An error of some description occurred while decoding the media resource, after the resource was established to be usable.
324 * Error code 4, MEDIA_ERR_SRC_NOT_SUPPORTED: Media (audio file) not supported ("not usable.")
325 * Reference: https://html.spec.whatwg.org/multipage/embedded-content.html#error-codes
326 */
327 html5ErrorCodes = [
328 null,
329 'MEDIA_ERR_ABORTED',
330 'MEDIA_ERR_NETWORK',
331 'MEDIA_ERR_DECODE',
332 'MEDIA_ERR_SRC_NOT_SUPPORTED'
333 ];
334
335 /**
336 * basic HTML5 Audio() support test
337 * try...catch because of IE 9 "not implemented" nonsense
338 * https://github.com/Modernizr/Modernizr/issues/224
339 */
340
341 this.hasHTML5 = (function() {
342 try {
343 // new Audio(null) for stupid Opera 9.64 case, which throws not_enough_arguments exception otherwise.
344 return (Audio !== _undefined && (isOpera && opera !== _undefined && opera.version() < 10 ? new Audio(null) : new Audio()).canPlayType !== _undefined);
345 } catch(e) {
346 return false;
347 }
348 }());
349
350 /**
351 * Public SoundManager API
352 * -----------------------
353 */
354
355 /**
356 * Configures top-level soundManager properties.
357 *
358 * @param {object} options Option parameters, eg. { flashVersion: 9, url: '/path/to/swfs/' }
359 * onready and ontimeout are also accepted parameters. call soundManager.setup() to see the full list.
360 */
361
362 this.setup = function(options) {
363
364 var noURL = (!sm2.url);
365
366 // warn if flash options have already been applied
367
368 if (options !== _undefined && didInit && needsFlash && sm2.ok() && (options.flashVersion !== _undefined || options.url !== _undefined || options.html5Test !== _undefined)) {
369 complain(str('setupLate'));
370 }
371
372 // TODO: defer: true?
373
374 assign(options);
375
376 if (!useGlobalHTML5Audio) {
377
378 if (mobileHTML5) {
379
380 // force the singleton HTML5 pattern on mobile, by default.
381 if (!sm2.setupOptions.ignoreMobileRestrictions || sm2.setupOptions.forceUseGlobalHTML5Audio) {
382 messages.push(strings.globalHTML5);
383 useGlobalHTML5Audio = true;
384 }
385
386 } else if (sm2.setupOptions.forceUseGlobalHTML5Audio) {
387
388 // only apply singleton HTML5 on desktop if forced.
389 messages.push(strings.globalHTML5);
390 useGlobalHTML5Audio = true;
391
392 }
393
394 }
395
396 if (!didSetup && mobileHTML5) {
397
398 if (sm2.setupOptions.ignoreMobileRestrictions) {
399
400 messages.push(strings.ignoreMobile);
401
402 } else {
403
404 // prefer HTML5 for mobile + tablet-like devices, probably more reliable vs. flash at this point.
405
406 // <d>
407 if (!sm2.setupOptions.useHTML5Audio || sm2.setupOptions.preferFlash) {
408 // notify that defaults are being changed.
409 sm2._wD(strings.mobileUA);
410 }
411 // </d>
412
413 sm2.setupOptions.useHTML5Audio = true;
414 sm2.setupOptions.preferFlash = false;
415
416 if (is_iDevice) {
417
418 // no flash here.
419 sm2.ignoreFlash = true;
420
421 } else if ((isAndroid && !ua.match(/android\s2\.3/i)) || !isAndroid) {
422
423 /**
424 * Android devices tend to work better with a single audio instance, specifically for chained playback of sounds in sequence.
425 * Common use case: exiting sound onfinish() -> createSound() -> play()
426 * Presuming similar restrictions for other mobile, non-Android, non-iOS devices.
427 */
428
429 // <d>
430 sm2._wD(strings.globalHTML5);
431 // </d>
432
433 useGlobalHTML5Audio = true;
434
435 }
436
437 }
438
439 }
440
441 // 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.
442
443 if (options) {
444
445 if (noURL && didDCLoaded && options.url !== _undefined) {
446 sm2.beginDelayedInit();
447 }
448
449 // special case 2: If lazy-loading SM2 (DOMContentLoaded has already happened) and user calls setup() with url: parameter, try to init ASAP.
450
451 if (!didDCLoaded && options.url !== _undefined && doc.readyState === 'complete') {
452 setTimeout(domContentLoaded, 1);
453 }
454
455 }
456
457 didSetup = true;
458
459 return sm2;
460
461 };
462
463 this.ok = function() {
464
465 return (needsFlash ? (didInit && !disabled) : (sm2.useHTML5Audio && sm2.hasHTML5));
466
467 };
468
469 this.supported = this.ok; // legacy
470
471 this.getMovie = function(movie_id) {
472
473 // safety net: some old browsers differ on SWF references, possibly related to ExternalInterface / flash version
474 return id(movie_id) || doc[movie_id] || window[movie_id];
475
476 };
477
478 /**
479 * Creates a SMSound sound object instance. Can also be overloaded, e.g., createSound('mySound', '/some.mp3');
480 *
481 * @param {object} oOptions Sound options (at minimum, url parameter is required.)
482 * @return {object} SMSound The new SMSound object.
483 */
484
485 this.createSound = function(oOptions, _url) {
486
487 var cs, cs_string, options, oSound = null;
488
489 // <d>
490 cs = sm + '.createSound(): ';
491 cs_string = cs + str(!didInit ? 'notReady' : 'notOK');
492 // </d>
493
494 if (!didInit || !sm2.ok()) {
495 complain(cs_string);
496 return false;
497 }
498
499 if (_url !== _undefined) {
500 // function overloading in JS! :) ... assume simple createSound(id, url) use case.
501 oOptions = {
502 id: oOptions,
503 url: _url
504 };
505 }
506
507 // inherit from defaultOptions
508 options = mixin(oOptions);
509
510 options.url = parseURL(options.url);
511
512 // generate an id, if needed.
513 if (options.id === _undefined) {
514 options.id = sm2.setupOptions.idPrefix + (idCounter++);
515 }
516
517 // <d>
518 if (options.id.toString().charAt(0).match(/^[0-9]$/)) {
519 sm2._wD(cs + str('badID', options.id), 2);
520 }
521
522 sm2._wD(cs + options.id + (options.url ? ' (' + options.url + ')' : ''), 1);
523 // </d>
524
525 if (idCheck(options.id, true)) {
526 sm2._wD(cs + options.id + ' exists', 1);
527 return sm2.sounds[options.id];
528 }
529
530 function make() {
531
532 options = loopFix(options);
533 sm2.sounds[options.id] = new SMSound(options);
534 sm2.soundIDs.push(options.id);
535 return sm2.sounds[options.id];
536
537 }
538
539 if (html5OK(options)) {
540
541 oSound = make();
542 // <d>
543 if (!sm2.html5Only) {
544 sm2._wD(options.id + ': Using HTML5');
545 }
546 // </d>
547 oSound._setup_html5(options);
548
549 } else {
550
551 if (sm2.html5Only) {
552 sm2._wD(options.id + ': No HTML5 support for this sound, and no Flash. Exiting.');
553 return make();
554 }
555
556 // TODO: Move HTML5/flash checks into generic URL parsing/handling function.
557
558 if (sm2.html5.usingFlash && options.url && options.url.match(/data:/i)) {
559 // data: URIs not supported by Flash, either.
560 sm2._wD(options.id + ': data: URIs not supported via Flash. Exiting.');
561 return make();
562 }
563
564 if (fV > 8) {
565 if (options.isMovieStar === null) {
566 // attempt to detect MPEG-4 formats
567 options.isMovieStar = !!(options.serverURL || (options.type ? options.type.match(netStreamMimeTypes) : false) || (options.url && options.url.match(netStreamPattern)));
568 }
569 // <d>
570 if (options.isMovieStar) {
571 sm2._wD(cs + 'using MovieStar handling');
572 if (options.loops > 1) {
573 _wDS('noNSLoop');
574 }
575 }
576 // </d>
577 }
578
579 options = policyFix(options, cs);
580 oSound = make();
581
582 if (fV === 8) {
583 flash._createSound(options.id, options.loops || 1, options.usePolicyFile);
584 } else {
585 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);
586 if (!options.serverURL) {
587 // We are connected immediately
588 oSound.connected = true;
589 if (options.onconnect) {
590 options.onconnect.apply(oSound);
591 }
592 }
593 }
594
595 if (!options.serverURL && (options.autoLoad || options.autoPlay)) {
596 // call load for non-rtmp streams
597 oSound.load(options);
598 }
599
600 }
601
602 // rtmp will play in onconnect
603 if (!options.serverURL && options.autoPlay) {
604 oSound.play();
605 }
606
607 return oSound;
608
609 };
610
611 /**
612 * Destroys a SMSound sound object instance.
613 *
614 * @param {string} sID The ID of the sound to destroy
615 */
616
617 this.destroySound = function(sID, _bFromSound) {
618
619 // explicitly destroy a sound before normal page unload, etc.
620
621 if (!idCheck(sID)) return false;
622
623 var oS = sm2.sounds[sID], i;
624
625 oS.stop();
626
627 // Disable all callbacks after stop(), when the sound is being destroyed
628 oS._iO = {};
629
630 oS.unload();
631
632 for (i = 0; i < sm2.soundIDs.length; i++) {
633 if (sm2.soundIDs[i] === sID) {
634 sm2.soundIDs.splice(i, 1);
635 break;
636 }
637 }
638
639 if (!_bFromSound) {
640 // ignore if being called from SMSound instance
641 oS.destruct(true);
642 }
643
644 oS = null;
645 delete sm2.sounds[sID];
646
647 return true;
648
649 };
650
651 /**
652 * Calls the load() method of a SMSound object by ID.
653 *
654 * @param {string} sID The ID of the sound
655 * @param {object} oOptions Optional: Sound options
656 */
657
658 this.load = function(sID, oOptions) {
659
660 if (!idCheck(sID)) return false;
661
662 return sm2.sounds[sID].load(oOptions);
663
664 };
665
666 /**
667 * Calls the unload() method of a SMSound object by ID.
668 *
669 * @param {string} sID The ID of the sound
670 */
671
672 this.unload = function(sID) {
673
674 if (!idCheck(sID)) return false;
675
676 return sm2.sounds[sID].unload();
677
678 };
679
680 /**
681 * Calls the onPosition() method of a SMSound object by ID.
682 *
683 * @param {string} sID The ID of the sound
684 * @param {number} nPosition The position to watch for
685 * @param {function} oMethod The relevant callback to fire
686 * @param {object} oScope Optional: The scope to apply the callback to
687 * @return {SMSound} The SMSound object
688 */
689
690 this.onPosition = function(sID, nPosition, oMethod, oScope) {
691
692 if (!idCheck(sID)) return false;
693
694 return sm2.sounds[sID].onposition(nPosition, oMethod, oScope);
695
696 };
697
698 // legacy/backwards-compability: lower-case method name
699 this.onposition = this.onPosition;
700
701 /**
702 * Calls the clearOnPosition() method of a SMSound object by ID.
703 *
704 * @param {string} sID The ID of the sound
705 * @param {number} nPosition The position to watch for
706 * @param {function} oMethod Optional: The relevant callback to fire
707 * @return {SMSound} The SMSound object
708 */
709
710 this.clearOnPosition = function(sID, nPosition, oMethod) {
711
712 if (!idCheck(sID)) return false;
713
714 return sm2.sounds[sID].clearOnPosition(nPosition, oMethod);
715
716 };
717
718 /**
719 * Calls the play() method of a SMSound object by ID.
720 *
721 * @param {string} sID The ID of the sound
722 * @param {object} oOptions Optional: Sound options
723 * @return {SMSound} The SMSound object
724 */
725
726 this.play = function(sID, oOptions) {
727
728 var result = null,
729 // legacy function-overloading use case: play('mySound', '/path/to/some.mp3');
730 overloaded = (oOptions && !(oOptions instanceof Object));
731
732 if (!didInit || !sm2.ok()) {
733 complain(sm + '.play(): ' + str(!didInit ? 'notReady' : 'notOK'));
734 return false;
735 }
736
737 if (!idCheck(sID, overloaded)) {
738
739 // no sound found for the given ID. Bail.
740 if (!overloaded) return false;
741
742 if (overloaded) {
743 oOptions = {
744 url: oOptions
745 };
746 }
747
748 if (oOptions && oOptions.url) {
749 // overloading use case, create+play: .play('someID', {url:'/path/to.mp3'});
750 sm2._wD(sm + '.play(): Attempting to create "' + sID + '"', 1);
751 oOptions.id = sID;
752 result = sm2.createSound(oOptions).play();
753 }
754
755 } else if (overloaded) {
756
757 // existing sound object case
758 oOptions = {
759 url: oOptions
760 };
761
762 }
763
764 if (result === null) {
765 // default case
766 result = sm2.sounds[sID].play(oOptions);
767 }
768
769 return result;
770
771 };
772
773 // just for convenience
774 this.start = this.play;
775
776 /**
777 * Calls the setPlaybackRate() method of a SMSound object by ID.
778 *
779 * @param {string} sID The ID of the sound
780 * @return {SMSound} The SMSound object
781 */
782
783 this.setPlaybackRate = function(sID, rate, allowOverride) {
784
785 if (!idCheck(sID)) return false;
786
787 return sm2.sounds[sID].setPlaybackRate(rate, allowOverride);
788
789 };
790
791 /**
792 * Calls the setPosition() method of a SMSound object by ID.
793 *
794 * @param {string} sID The ID of the sound
795 * @param {number} nMsecOffset Position (milliseconds)
796 * @return {SMSound} The SMSound object
797 */
798
799 this.setPosition = function(sID, nMsecOffset) {
800
801 if (!idCheck(sID)) return false;
802
803 return sm2.sounds[sID].setPosition(nMsecOffset);
804
805 };
806
807 /**
808 * Calls the stop() method of a SMSound object by ID.
809 *
810 * @param {string} sID The ID of the sound
811 * @return {SMSound} The SMSound object
812 */
813
814 this.stop = function(sID) {
815
816 if (!idCheck(sID)) return false;
817
818 sm2._wD(sm + '.stop(' + sID + ')', 1);
819
820 return sm2.sounds[sID].stop();
821
822 };
823
824 /**
825 * Stops all currently-playing sounds.
826 */
827
828 this.stopAll = function() {
829
830 var oSound;
831 sm2._wD(sm + '.stopAll()', 1);
832
833 for (oSound in sm2.sounds) {
834 if (sm2.sounds.hasOwnProperty(oSound)) {
835 // apply only to sound objects
836 sm2.sounds[oSound].stop();
837 }
838 }
839
840 };
841
842 /**
843 * Calls the pause() method of a SMSound object by ID.
844 *
845 * @param {string} sID The ID of the sound
846 * @return {SMSound} The SMSound object
847 */
848
849 this.pause = function(sID) {
850
851 if (!idCheck(sID)) return false;
852
853 return sm2.sounds[sID].pause();
854
855 };
856
857 /**
858 * Pauses all currently-playing sounds.
859 */
860
861 this.pauseAll = function() {
862
863 var i;
864 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
865 sm2.sounds[sm2.soundIDs[i]].pause();
866 }
867
868 };
869
870 /**
871 * Calls the resume() method of a SMSound object by ID.
872 *
873 * @param {string} sID The ID of the sound
874 * @return {SMSound} The SMSound object
875 */
876
877 this.resume = function(sID) {
878
879 if (!idCheck(sID)) return false;
880
881 return sm2.sounds[sID].resume();
882
883 };
884
885 /**
886 * Resumes all currently-paused sounds.
887 */
888
889 this.resumeAll = function() {
890
891 var i;
892 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
893 sm2.sounds[sm2.soundIDs[i]].resume();
894 }
895
896 };
897
898 /**
899 * Calls the togglePause() method of a SMSound object by ID.
900 *
901 * @param {string} sID The ID of the sound
902 * @return {SMSound} The SMSound object
903 */
904
905 this.togglePause = function(sID) {
906
907 if (!idCheck(sID)) return false;
908
909 return sm2.sounds[sID].togglePause();
910
911 };
912
913 /**
914 * Calls the setPan() method of a SMSound object by ID.
915 *
916 * @param {string} sID The ID of the sound
917 * @param {number} nPan The pan value (-100 to 100)
918 * @return {SMSound} The SMSound object
919 */
920
921 this.setPan = function(sID, nPan) {
922
923 if (!idCheck(sID)) return false;
924
925 return sm2.sounds[sID].setPan(nPan);
926
927 };
928
929 /**
930 * Calls the setVolume() method of a SMSound object by ID
931 * Overloaded case: pass only volume argument eg., setVolume(50) to apply to all sounds.
932 *
933 * @param {string} sID The ID of the sound
934 * @param {number} nVol The volume value (0 to 100)
935 * @return {SMSound} The SMSound object
936 */
937
938 this.setVolume = function(sID, nVol) {
939
940 // setVolume(50) function overloading case - apply to all sounds
941
942 var i, j;
943
944 if (sID !== _undefined && !isNaN(sID) && nVol === _undefined) {
945 for (i = 0, j = sm2.soundIDs.length; i < j; i++) {
946 sm2.sounds[sm2.soundIDs[i]].setVolume(sID);
947 }
948 return false;
949 }
950
951 // setVolume('mySound', 50) case
952
953 if (!idCheck(sID)) return false;
954
955 return sm2.sounds[sID].setVolume(nVol);
956
957 };
958
959 /**
960 * Calls the mute() method of either a single SMSound object by ID, or all sound objects.
961 *
962 * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
963 */
964
965 this.mute = function(sID) {
966
967 var i = 0;
968
969 if (sID instanceof String) {
970 sID = null;
971 }
972
973 if (!sID) {
974
975 sm2._wD(sm + '.mute(): Muting all sounds');
976 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
977 sm2.sounds[sm2.soundIDs[i]].mute();
978 }
979 sm2.muted = true;
980
981 } else {
982
983 if (!idCheck(sID)) return false;
984
985 sm2._wD(sm + '.mute(): Muting "' + sID + '"');
986 return sm2.sounds[sID].mute();
987
988 }
989
990 return true;
991
992 };
993
994 /**
995 * Mutes all sounds.
996 */
997
998 this.muteAll = function() {
999
1000 sm2.mute();
1001
1002 };
1003
1004 /**
1005 * Calls the unmute() method of either a single SMSound object by ID, or all sound objects.
1006 *
1007 * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.)
1008 */
1009
1010 this.unmute = function(sID) {
1011
1012 var i;
1013
1014 if (sID instanceof String) {
1015 sID = null;
1016 }
1017
1018 if (!sID) {
1019
1020 sm2._wD(sm + '.unmute(): Unmuting all sounds');
1021 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
1022 sm2.sounds[sm2.soundIDs[i]].unmute();
1023 }
1024 sm2.muted = false;
1025
1026 } else {
1027
1028 if (!idCheck(sID)) return false;
1029
1030 sm2._wD(sm + '.unmute(): Unmuting "' + sID + '"');
1031
1032 return sm2.sounds[sID].unmute();
1033
1034 }
1035
1036 return true;
1037
1038 };
1039
1040 /**
1041 * Unmutes all sounds.
1042 */
1043
1044 this.unmuteAll = function() {
1045
1046 sm2.unmute();
1047
1048 };
1049
1050 /**
1051 * Calls the toggleMute() method of a SMSound object by ID.
1052 *
1053 * @param {string} sID The ID of the sound
1054 * @return {SMSound} The SMSound object
1055 */
1056
1057 this.toggleMute = function(sID) {
1058
1059 if (!idCheck(sID)) return false;
1060
1061 return sm2.sounds[sID].toggleMute();
1062
1063 };
1064
1065 /**
1066 * Retrieves the memory used by the flash plugin.
1067 *
1068 * @return {number} The amount of memory in use
1069 */
1070
1071 this.getMemoryUse = function() {
1072
1073 // flash-only
1074 var ram = 0;
1075
1076 if (flash && fV !== 8) {
1077 ram = parseInt(flash._getMemoryUse(), 10);
1078 }
1079
1080 return ram;
1081
1082 };
1083
1084 /**
1085 * Undocumented: NOPs soundManager and all SMSound objects.
1086 */
1087
1088 this.disable = function(bNoDisable) {
1089
1090 // destroy all functions
1091 var i;
1092
1093 if (bNoDisable === _undefined) {
1094 bNoDisable = false;
1095 }
1096
1097 // already disabled?
1098 if (disabled) return false;
1099
1100 disabled = true;
1101
1102 _wDS('shutdown', 1);
1103
1104 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
1105 disableObject(sm2.sounds[sm2.soundIDs[i]]);
1106 }
1107
1108 disableObject(sm2);
1109
1110 // fire "complete", despite fail
1111 initComplete(bNoDisable);
1112
1113 event.remove(window, 'load', initUserOnload);
1114
1115 return true;
1116
1117 };
1118
1119 /**
1120 * Determines playability of a MIME type, eg. 'audio/mp3'.
1121 */
1122
1123 this.canPlayMIME = function(sMIME) {
1124
1125 var result;
1126
1127 if (sm2.hasHTML5) {
1128 result = html5CanPlay({
1129 type: sMIME
1130 });
1131 }
1132
1133 if (!result && needsFlash) {
1134 // if flash 9, test netStream (movieStar) types as well.
1135 result = (sMIME && sm2.ok() ? !!((fV > 8 ? sMIME.match(netStreamMimeTypes) : null) || sMIME.match(sm2.mimePattern)) : null); // TODO: make less "weird" (per JSLint)
1136 }
1137
1138 return result;
1139
1140 };
1141
1142 /**
1143 * Determines playability of a URL based on audio support.
1144 *
1145 * @param {string} sURL The URL to test
1146 * @return {boolean} URL playability
1147 */
1148
1149 this.canPlayURL = function(sURL) {
1150
1151 var result;
1152
1153 if (sm2.hasHTML5) {
1154 result = html5CanPlay({
1155 url: sURL
1156 });
1157 }
1158
1159 if (!result && needsFlash) {
1160 result = (sURL && sm2.ok() ? !!(sURL.match(sm2.filePattern)) : null);
1161 }
1162
1163 return result;
1164
1165 };
1166
1167 /**
1168 * Determines playability of an HTML DOM <a> object (or similar object literal) based on audio support.
1169 *
1170 * @param {object} oLink an HTML DOM <a> object or object literal including href and/or type attributes
1171 * @return {boolean} URL playability
1172 */
1173
1174 this.canPlayLink = function(oLink) {
1175
1176 if (oLink.type !== _undefined && oLink.type && sm2.canPlayMIME(oLink.type)) return true;
1177
1178 return sm2.canPlayURL(oLink.href);
1179
1180 };
1181
1182 /**
1183 * Retrieves a SMSound object by ID.
1184 *
1185 * @param {string} sID The ID of the sound
1186 * @return {SMSound} The SMSound object
1187 */
1188
1189 this.getSoundById = function(sID, _suppressDebug) {
1190
1191 if (!sID) return null;
1192
1193 var result = sm2.sounds[sID];
1194
1195 // <d>
1196 if (!result && !_suppressDebug) {
1197 sm2._wD(sm + '.getSoundById(): Sound "' + sID + '" not found.', 2);
1198 }
1199 // </d>
1200
1201 return result;
1202
1203 };
1204
1205 /**
1206 * Queues a callback for execution when SoundManager has successfully initialized.
1207 *
1208 * @param {function} oMethod The callback method to fire
1209 * @param {object} oScope Optional: The scope to apply to the callback
1210 */
1211
1212 this.onready = function(oMethod, oScope) {
1213
1214 var sType = 'onready',
1215 result = false;
1216
1217 if (typeof oMethod === 'function') {
1218
1219 // <d>
1220 if (didInit) {
1221 sm2._wD(str('queue', sType));
1222 }
1223 // </d>
1224
1225 if (!oScope) {
1226 oScope = window;
1227 }
1228
1229 addOnEvent(sType, oMethod, oScope);
1230 processOnEvents();
1231
1232 result = true;
1233
1234 } else {
1235
1236 throw str('needFunction', sType);
1237
1238 }
1239
1240 return result;
1241
1242 };
1243
1244 /**
1245 * Queues a callback for execution when SoundManager has failed to initialize.
1246 *
1247 * @param {function} oMethod The callback method to fire
1248 * @param {object} oScope Optional: The scope to apply to the callback
1249 */
1250
1251 this.ontimeout = function(oMethod, oScope) {
1252
1253 var sType = 'ontimeout',
1254 result = false;
1255
1256 if (typeof oMethod === 'function') {
1257
1258 // <d>
1259 if (didInit) {
1260 sm2._wD(str('queue', sType));
1261 }
1262 // </d>
1263
1264 if (!oScope) {
1265 oScope = window;
1266 }
1267
1268 addOnEvent(sType, oMethod, oScope);
1269 processOnEvents({ type: sType });
1270
1271 result = true;
1272
1273 } else {
1274
1275 throw str('needFunction', sType);
1276
1277 }
1278
1279 return result;
1280
1281 };
1282
1283 /**
1284 * Writes console.log()-style debug output to a console or in-browser element.
1285 * Applies when debugMode = true
1286 *
1287 * @param {string} sText The console message
1288 * @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.
1289 */
1290
1291 this._writeDebug = function(sText, sTypeOrObject) {
1292
1293 // pseudo-private console.log()-style output
1294 // <d>
1295
1296 var sDID = 'soundmanager-debug', o, oItem;
1297
1298 if (!sm2.setupOptions.debugMode) return false;
1299
1300 if (hasConsole && sm2.useConsole) {
1301 if (sTypeOrObject && typeof sTypeOrObject === 'object') {
1302 // object passed; dump to console.
1303 console.log(sText, sTypeOrObject);
1304 } else if (debugLevels[sTypeOrObject] !== _undefined) {
1305 console[debugLevels[sTypeOrObject]](sText);
1306 } else {
1307 console.log(sText);
1308 }
1309 if (sm2.consoleOnly) return true;
1310 }
1311
1312 o = id(sDID);
1313
1314 if (!o) return false;
1315
1316 oItem = doc.createElement('div');
1317
1318 if (++wdCount % 2 === 0) {
1319 oItem.className = 'sm2-alt';
1320 }
1321
1322 if (sTypeOrObject === _undefined) {
1323 sTypeOrObject = 0;
1324 } else {
1325 sTypeOrObject = parseInt(sTypeOrObject, 10);
1326 }
1327
1328 oItem.appendChild(doc.createTextNode(sText));
1329
1330 if (sTypeOrObject) {
1331 if (sTypeOrObject >= 2) {
1332 oItem.style.fontWeight = 'bold';
1333 }
1334 if (sTypeOrObject === 3) {
1335 oItem.style.color = '#ff3333';
1336 }
1337 }
1338
1339 // top-to-bottom
1340 // o.appendChild(oItem);
1341
1342 // bottom-to-top
1343 o.insertBefore(oItem, o.firstChild);
1344
1345 o = null;
1346 // </d>
1347
1348 return true;
1349
1350 };
1351
1352 // <d>
1353 // last-resort debugging option
1354 if (wl.indexOf('sm2-debug=alert') !== -1) {
1355 this._writeDebug = function(sText) {
1356 window.alert(sText);
1357 };
1358 }
1359 // </d>
1360
1361 // alias
1362 this._wD = this._writeDebug;
1363
1364 /**
1365 * Provides debug / state information on all SMSound objects.
1366 */
1367
1368 this._debug = function() {
1369
1370 // <d>
1371 var i, j;
1372 _wDS('currentObj', 1);
1373
1374 for (i = 0, j = sm2.soundIDs.length; i < j; i++) {
1375 sm2.sounds[sm2.soundIDs[i]]._debug();
1376 }
1377 // </d>
1378
1379 };
1380
1381 /**
1382 * Restarts and re-initializes the SoundManager instance.
1383 *
1384 * @param {boolean} resetEvents Optional: When true, removes all registered onready and ontimeout event callbacks.
1385 * @param {boolean} excludeInit Options: When true, does not call beginDelayedInit() (which would restart SM2).
1386 * @return {object} soundManager The soundManager instance.
1387 */
1388
1389 this.reboot = function(resetEvents, excludeInit) {
1390
1391 // reset some (or all) state, and re-init unless otherwise specified.
1392
1393 // <d>
1394 if (sm2.soundIDs.length) {
1395 sm2._wD('Destroying ' + sm2.soundIDs.length + ' SMSound object' + (sm2.soundIDs.length !== 1 ? 's' : '') + '...');
1396 }
1397 // </d>
1398
1399 var i, j, k;
1400
1401 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
1402 sm2.sounds[sm2.soundIDs[i]].destruct();
1403 }
1404
1405 // trash ze flash (remove from the DOM)
1406
1407 if (flash) {
1408
1409 try {
1410
1411 if (isIE) {
1412 oRemovedHTML = flash.innerHTML;
1413 }
1414
1415 oRemoved = flash.parentNode.removeChild(flash);
1416
1417 } catch(e) {
1418
1419 // Remove failed? May be due to flash blockers silently removing the SWF object/embed node from the DOM. Warn and continue.
1420
1421 _wDS('badRemove', 2);
1422
1423 }
1424
1425 }
1426
1427 // actually, force recreate of movie.
1428
1429 oRemovedHTML = oRemoved = needsFlash = flash = null;
1430
1431 sm2.enabled = didDCLoaded = didInit = waitingForEI = initPending = didAppend = appendSuccess = disabled = useGlobalHTML5Audio = sm2.swfLoaded = false;
1432
1433 sm2.soundIDs = [];
1434 sm2.sounds = {};
1435
1436 idCounter = 0;
1437 didSetup = false;
1438
1439 if (!resetEvents) {
1440 // reset callbacks for onready, ontimeout etc. so that they will fire again on re-init
1441 for (i in on_queue) {
1442 if (on_queue.hasOwnProperty(i)) {
1443 for (j = 0, k = on_queue[i].length; j < k; j++) {
1444 on_queue[i][j].fired = false;
1445 }
1446 }
1447 }
1448 } else {
1449 // remove all callbacks entirely
1450 on_queue = [];
1451 }
1452
1453 // <d>
1454 if (!excludeInit) {
1455 sm2._wD(sm + ': Rebooting...');
1456 }
1457 // </d>
1458
1459 // reset HTML5 and flash canPlay test results
1460
1461 sm2.html5 = {
1462 usingFlash: null
1463 };
1464
1465 sm2.flash = {};
1466
1467 // reset device-specific HTML/flash mode switches
1468
1469 sm2.html5Only = false;
1470 sm2.ignoreFlash = false;
1471
1472 window.setTimeout(function() {
1473
1474 // by default, re-init
1475
1476 if (!excludeInit) {
1477 sm2.beginDelayedInit();
1478 }
1479
1480 }, 20);
1481
1482 return sm2;
1483
1484 };
1485
1486 this.reset = function() {
1487
1488 /**
1489 * Shuts down and restores the SoundManager instance to its original loaded state, without an explicit reboot. All onready/ontimeout handlers are removed.
1490 * After this call, SM2 may be re-initialized via soundManager.beginDelayedInit().
1491 * @return {object} soundManager The soundManager instance.
1492 */
1493
1494 _wDS('reset');
1495
1496 return sm2.reboot(true, true);
1497
1498 };
1499
1500 /**
1501 * Undocumented: Determines the SM2 flash movie's load progress.
1502 *
1503 * @return {number or null} Percent loaded, or if invalid/unsupported, null.
1504 */
1505
1506 this.getMoviePercent = function() {
1507
1508 /**
1509 * Interesting syntax notes...
1510 * Flash/ExternalInterface (ActiveX/NPAPI) bridge methods are not typeof "function" nor instanceof Function, but are still valid.
1511 * Furthermore, using (flash && flash.PercentLoaded) causes IE to throw "object doesn't support this property or method".
1512 * Thus, 'in' syntax must be used.
1513 */
1514
1515 return (flash && 'PercentLoaded' in flash ? flash.PercentLoaded() : null);
1516
1517 };
1518
1519 /**
1520 * Additional helper for manually invoking SM2's init process after DOM Ready / window.onload().
1521 */
1522
1523 this.beginDelayedInit = function() {
1524
1525 windowLoaded = true;
1526 domContentLoaded();
1527
1528 setTimeout(function() {
1529
1530 if (initPending) return false;
1531
1532 createMovie();
1533 initMovie();
1534 initPending = true;
1535
1536 return true;
1537
1538 }, 20);
1539
1540 delayWaitForEI();
1541
1542 };
1543
1544 /**
1545 * Destroys the SoundManager instance and all SMSound instances.
1546 */
1547
1548 this.destruct = function() {
1549
1550 sm2._wD(sm + '.destruct()');
1551 sm2.disable(true);
1552
1553 };
1554
1555 /**
1556 * SMSound() (sound object) constructor
1557 * ------------------------------------
1558 *
1559 * @param {object} oOptions Sound options (id and url are required attributes)
1560 * @return {SMSound} The new SMSound object
1561 */
1562
1563 SMSound = function(oOptions) {
1564
1565 var s = this, resetProperties, add_html5_events, remove_html5_events, stop_html5_timer, start_html5_timer, attachOnPosition, onplay_called = false, onPositionItems = [], onPositionFired = 0, detachOnPosition, applyFromTo, lastURL = null, lastHTML5State, urlOmitted;
1566
1567 lastHTML5State = {
1568 // tracks duration + position (time)
1569 duration: null,
1570 time: null
1571 };
1572
1573 this.id = oOptions.id;
1574
1575 // legacy
1576 this.sID = this.id;
1577
1578 this.url = oOptions.url;
1579 this.options = mixin(oOptions);
1580
1581 // per-play-instance-specific options
1582 this.instanceOptions = this.options;
1583
1584 // short alias
1585 this._iO = this.instanceOptions;
1586
1587 // assign property defaults
1588 this.pan = this.options.pan;
1589 this.volume = this.options.volume;
1590
1591 // whether or not this object is using HTML5
1592 this.isHTML5 = false;
1593
1594 // internal HTML5 Audio() object reference
1595 this._a = null;
1596
1597 // for flash 8 special-case createSound() without url, followed by load/play with url case
1598 urlOmitted = (!this.url);
1599
1600 /**
1601 * SMSound() public methods
1602 * ------------------------
1603 */
1604
1605 this.id3 = {};
1606
1607 /**
1608 * Writes SMSound object parameters to debug console
1609 */
1610
1611 this._debug = function() {
1612
1613 // <d>
1614 sm2._wD(s.id + ': Merged options:', s.options);
1615 // </d>
1616
1617 };
1618
1619 /**
1620 * Begins loading a sound per its *url*.
1621 *
1622 * @param {object} options Optional: Sound options
1623 * @return {SMSound} The SMSound object
1624 */
1625
1626 this.load = function(options) {
1627
1628 var oSound = null, instanceOptions;
1629
1630 if (options !== _undefined) {
1631 s._iO = mixin(options, s.options);
1632 } else {
1633 options = s.options;
1634 s._iO = options;
1635 if (lastURL && lastURL !== s.url) {
1636 _wDS('manURL');
1637 s._iO.url = s.url;
1638 s.url = null;
1639 }
1640 }
1641
1642 if (!s._iO.url) {
1643 s._iO.url = s.url;
1644 }
1645
1646 s._iO.url = parseURL(s._iO.url);
1647
1648 // ensure we're in sync
1649 s.instanceOptions = s._iO;
1650
1651 // local shortcut
1652 instanceOptions = s._iO;
1653
1654 sm2._wD(s.id + ': load (' + instanceOptions.url + ')');
1655
1656 if (!instanceOptions.url && !s.url) {
1657 sm2._wD(s.id + ': load(): url is unassigned. Exiting.', 2);
1658 return s;
1659 }
1660
1661 // <d>
1662 if (!s.isHTML5 && fV === 8 && !s.url && !instanceOptions.autoPlay) {
1663 // flash 8 load() -> play() won't work before onload has fired.
1664 sm2._wD(s.id + ': Flash 8 load() limitation: Wait for onload() before calling play().', 1);
1665 }
1666 // </d>
1667
1668 if (instanceOptions.url === s.url && s.readyState !== 0 && s.readyState !== 2) {
1669 _wDS('onURL', 1);
1670 // if loaded and an onload() exists, fire immediately.
1671 if (s.readyState === 3 && instanceOptions.onload) {
1672 // assume success based on truthy duration.
1673 wrapCallback(s, function() {
1674 instanceOptions.onload.apply(s, [(!!s.duration)]);
1675 });
1676 }
1677 return s;
1678 }
1679
1680 // reset a few state properties
1681
1682 s.loaded = false;
1683 s.readyState = 1;
1684 s.playState = 0;
1685 s.id3 = {};
1686
1687 // TODO: If switching from HTML5 -> flash (or vice versa), stop currently-playing audio.
1688
1689 if (html5OK(instanceOptions)) {
1690
1691 oSound = s._setup_html5(instanceOptions);
1692
1693 if (!oSound._called_load) {
1694
1695 s._html5_canplay = false;
1696
1697 // TODO: review called_load / html5_canplay logic
1698
1699 // if url provided directly to load(), assign it here.
1700
1701 if (s.url !== instanceOptions.url) {
1702
1703 sm2._wD(_wDS('manURL') + ': ' + instanceOptions.url);
1704
1705 s._a.src = instanceOptions.url;
1706
1707 // TODO: review / re-apply all relevant options (volume, loop, onposition etc.)
1708
1709 // reset position for new URL
1710 s.setPosition(0);
1711
1712 }
1713
1714 // given explicit load call, try to preload.
1715
1716 // early HTML5 implementation (non-standard)
1717 s._a.autobuffer = 'auto';
1718
1719 // standard property, values: none / metadata / auto
1720 // reference: http://msdn.microsoft.com/en-us/library/ie/ff974759%28v=vs.85%29.aspx
1721 s._a.preload = 'auto';
1722
1723 s._a._called_load = true;
1724
1725 } else {
1726
1727 sm2._wD(s.id + ': Ignoring request to load again');
1728
1729 }
1730
1731 } else {
1732
1733 if (sm2.html5Only) {
1734 sm2._wD(s.id + ': No flash support. Exiting.');
1735 return s;
1736 }
1737
1738 if (s._iO.url && s._iO.url.match(/data:/i)) {
1739 // data: URIs not supported by Flash, either.
1740 sm2._wD(s.id + ': data: URIs not supported via Flash. Exiting.');
1741 return s;
1742 }
1743
1744 try {
1745 s.isHTML5 = false;
1746 s._iO = policyFix(loopFix(instanceOptions));
1747 // if we have "position", disable auto-play as we'll be seeking to that position at onload().
1748 if (s._iO.autoPlay && (s._iO.position || s._iO.from)) {
1749 sm2._wD(s.id + ': Disabling autoPlay because of non-zero offset case');
1750 s._iO.autoPlay = false;
1751 }
1752 // re-assign local shortcut
1753 instanceOptions = s._iO;
1754 if (fV === 8) {
1755 flash._load(s.id, instanceOptions.url, instanceOptions.stream, instanceOptions.autoPlay, instanceOptions.usePolicyFile);
1756 } else {
1757 flash._load(s.id, instanceOptions.url, !!(instanceOptions.stream), !!(instanceOptions.autoPlay), instanceOptions.loops || 1, !!(instanceOptions.autoLoad), instanceOptions.usePolicyFile);
1758 }
1759 } catch(e) {
1760 _wDS('smError', 2);
1761 debugTS('onload', false);
1762 catchError({
1763 type: 'SMSOUND_LOAD_JS_EXCEPTION',
1764 fatal: true
1765 });
1766 }
1767
1768 }
1769
1770 // after all of this, ensure sound url is up to date.
1771 s.url = instanceOptions.url;
1772
1773 return s;
1774
1775 };
1776
1777 /**
1778 * Unloads a sound, canceling any open HTTP requests.
1779 *
1780 * @return {SMSound} The SMSound object
1781 */
1782
1783 this.unload = function() {
1784
1785 // Flash 8/AS2 can't "close" a stream - fake it by loading an empty URL
1786 // Flash 9/AS3: Close stream, preventing further load
1787 // HTML5: Most UAs will use empty URL
1788
1789 if (s.readyState !== 0) {
1790
1791 sm2._wD(s.id + ': unload()');
1792
1793 if (!s.isHTML5) {
1794
1795 if (fV === 8) {
1796 flash._unload(s.id, emptyURL);
1797 } else {
1798 flash._unload(s.id);
1799 }
1800
1801 } else {
1802
1803 stop_html5_timer();
1804
1805 if (s._a) {
1806
1807 s._a.pause();
1808
1809 // update empty URL, too
1810 lastURL = html5Unload(s._a);
1811
1812 }
1813
1814 }
1815
1816 // reset load/status flags
1817 resetProperties();
1818
1819 }
1820
1821 return s;
1822
1823 };
1824
1825 /**
1826 * Unloads and destroys a sound.
1827 */
1828
1829 this.destruct = function(_bFromSM) {
1830
1831 sm2._wD(s.id + ': Destruct');
1832
1833 if (!s.isHTML5) {
1834
1835 // kill sound within Flash
1836 // Disable the onfailure handler
1837 s._iO.onfailure = null;
1838 flash._destroySound(s.id);
1839
1840 } else {
1841
1842 stop_html5_timer();
1843
1844 if (s._a) {
1845 s._a.pause();
1846 html5Unload(s._a);
1847 if (!useGlobalHTML5Audio) {
1848 remove_html5_events();
1849 }
1850 // break obvious circular reference
1851 s._a._s = null;
1852 s._a = null;
1853 }
1854
1855 }
1856
1857 if (!_bFromSM) {
1858 // ensure deletion from controller
1859 sm2.destroySound(s.id, true);
1860 }
1861
1862 };
1863
1864 /**
1865 * Begins playing a sound.
1866 *
1867 * @param {object} options Optional: Sound options
1868 * @return {SMSound} The SMSound object
1869 */
1870
1871 this.play = function(options, _updatePlayState) {
1872
1873 var fN, allowMulti, a, onready,
1874 audioClone, onended, oncanplay,
1875 startOK = true;
1876
1877 // <d>
1878 fN = s.id + ': play(): ';
1879 // </d>
1880
1881 // default to true
1882 _updatePlayState = (_updatePlayState === _undefined ? true : _updatePlayState);
1883
1884 if (!options) {
1885 options = {};
1886 }
1887
1888 // first, use local URL (if specified)
1889 if (s.url) {
1890 s._iO.url = s.url;
1891 }
1892
1893 // mix in any options defined at createSound()
1894 s._iO = mixin(s._iO, s.options);
1895
1896 // mix in any options specific to this method
1897 s._iO = mixin(options, s._iO);
1898
1899 s._iO.url = parseURL(s._iO.url);
1900
1901 s.instanceOptions = s._iO;
1902
1903 // RTMP-only
1904 if (!s.isHTML5 && s._iO.serverURL && !s.connected) {
1905 if (!s.getAutoPlay()) {
1906 sm2._wD(fN + ' Netstream not connected yet - setting autoPlay');
1907 s.setAutoPlay(true);
1908 }
1909 // play will be called in onconnect()
1910 return s;
1911 }
1912
1913 if (html5OK(s._iO)) {
1914 s._setup_html5(s._iO);
1915 start_html5_timer();
1916 }
1917
1918 if (s.playState === 1 && !s.paused) {
1919
1920 allowMulti = s._iO.multiShot;
1921
1922 if (!allowMulti) {
1923
1924 sm2._wD(fN + 'Already playing (one-shot)', 1);
1925
1926 if (s.isHTML5) {
1927 // go back to original position.
1928 s.setPosition(s._iO.position);
1929 }
1930
1931 return s;
1932
1933 }
1934
1935 sm2._wD(fN + 'Already playing (multi-shot)', 1);
1936
1937 }
1938
1939 // edge case: play() with explicit URL parameter
1940 if (options.url && options.url !== s.url) {
1941
1942 // special case for createSound() followed by load() / play() with url; avoid double-load case.
1943 if (!s.readyState && !s.isHTML5 && fV === 8 && urlOmitted) {
1944
1945 urlOmitted = false;
1946
1947 } else {
1948
1949 // load using merged options
1950 s.load(s._iO);
1951
1952 }
1953
1954 }
1955
1956 if (!s.loaded) {
1957
1958 if (s.readyState === 0) {
1959
1960 sm2._wD(fN + 'Attempting to load');
1961
1962 // try to get this sound playing ASAP
1963 if (!s.isHTML5 && !sm2.html5Only) {
1964
1965 // flash: assign directly because setAutoPlay() increments the instanceCount
1966 s._iO.autoPlay = true;
1967 s.load(s._iO);
1968
1969 } else if (s.isHTML5) {
1970
1971 // iOS needs this when recycling sounds, loading a new URL on an existing object.
1972 s.load(s._iO);
1973
1974 } else {
1975
1976 sm2._wD(fN + 'Unsupported type. Exiting.');
1977
1978 return s;
1979
1980 }
1981
1982 // HTML5 hack - re-set instanceOptions?
1983 s.instanceOptions = s._iO;
1984
1985 } else if (s.readyState === 2) {
1986
1987 sm2._wD(fN + 'Could not load - exiting', 2);
1988
1989 return s;
1990
1991 } else {
1992
1993 sm2._wD(fN + 'Loading - attempting to play...');
1994
1995 }
1996
1997 } else {
1998
1999 // "play()"
2000 sm2._wD(fN.substr(0, fN.lastIndexOf(':')));
2001
2002 }
2003
2004 if (!s.isHTML5 && fV === 9 && s.position > 0 && s.position === s.duration) {
2005 // flash 9 needs a position reset if play() is called while at the end of a sound.
2006 sm2._wD(fN + 'Sound at end, resetting to position: 0');
2007 options.position = 0;
2008 }
2009
2010 /**
2011 * Streams will pause when their buffer is full if they are being loaded.
2012 * In this case paused is true, but the song hasn't started playing yet.
2013 * If we just call resume() the onplay() callback will never be called.
2014 * So only call resume() if the position is > 0.
2015 * Another reason is because options like volume won't have been applied yet.
2016 * For normal sounds, just resume.
2017 */
2018
2019 if (s.paused && s.position >= 0 && (!s._iO.serverURL || s.position > 0)) {
2020
2021 // https://gist.github.com/37b17df75cc4d7a90bf6
2022 sm2._wD(fN + 'Resuming from paused state', 1);
2023 s.resume();
2024
2025 } else {
2026
2027 s._iO = mixin(options, s._iO);
2028
2029 /**
2030 * Preload in the event of play() with position under Flash,
2031 * or from/to parameters and non-RTMP case
2032 */
2033 if (((!s.isHTML5 && s._iO.position !== null && s._iO.position > 0) || (s._iO.from !== null && s._iO.from > 0) || s._iO.to !== null) && s.instanceCount === 0 && s.playState === 0 && !s._iO.serverURL) {
2034
2035 onready = function() {
2036 // sound "canplay" or onload()
2037 // re-apply position/from/to to instance options, and start playback
2038 s._iO = mixin(options, s._iO);
2039 s.play(s._iO);
2040 };
2041
2042 // HTML5 needs to at least have "canplay" fired before seeking.
2043 if (s.isHTML5 && !s._html5_canplay) {
2044
2045 // this hasn't been loaded yet. load it first, and then do this again.
2046 sm2._wD(fN + 'Beginning load for non-zero offset case');
2047
2048 s.load({
2049 // note: custom HTML5-only event added for from/to implementation.
2050 _oncanplay: onready
2051 });
2052
2053 } else if (!s.isHTML5 && !s.loaded && (!s.readyState || s.readyState !== 2)) {
2054
2055 // to be safe, preload the whole thing in Flash.
2056
2057 sm2._wD(fN + 'Preloading for non-zero offset case');
2058
2059 s.load({
2060 onload: onready
2061 });
2062
2063 }
2064
2065 // otherwise, we're ready to go. re-apply local options, and continue
2066
2067 s._iO = applyFromTo();
2068
2069 }
2070
2071 // sm2._wD(fN + 'Starting to play');
2072
2073 // increment instance counter, where enabled + supported
2074 if (!s.instanceCount || s._iO.multiShotEvents || (s.isHTML5 && s._iO.multiShot && !useGlobalHTML5Audio) || (!s.isHTML5 && fV > 8 && !s.getAutoPlay())) {
2075 s.instanceCount++;
2076 }
2077
2078 // if first play and onposition parameters exist, apply them now
2079 if (s._iO.onposition && s.playState === 0) {
2080 attachOnPosition(s);
2081 }
2082
2083 s.playState = 1;
2084 s.paused = false;
2085
2086 s.position = (s._iO.position !== _undefined && !isNaN(s._iO.position) ? s._iO.position : 0);
2087
2088 if (!s.isHTML5) {
2089 s._iO = policyFix(loopFix(s._iO));
2090 }
2091
2092 if (s._iO.onplay && _updatePlayState) {
2093 s._iO.onplay.apply(s);
2094 onplay_called = true;
2095 }
2096
2097 s.setVolume(s._iO.volume, true);
2098 s.setPan(s._iO.pan, true);
2099
2100 if (s._iO.playbackRate !== 1) {
2101 s.setPlaybackRate(s._iO.playbackRate);
2102 }
2103
2104 if (!s.isHTML5) {
2105
2106 startOK = flash._start(s.id, s._iO.loops || 1, (fV === 9 ? s.position : s.position / msecScale), s._iO.multiShot || false);
2107
2108 if (fV === 9 && !startOK) {
2109 // edge case: no sound hardware, or 32-channel flash ceiling hit.
2110 // applies only to Flash 9, non-NetStream/MovieStar sounds.
2111 // http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/Sound.html#play%28%29
2112 sm2._wD(fN + 'No sound hardware, or 32-sound ceiling hit', 2);
2113 if (s._iO.onplayerror) {
2114 s._iO.onplayerror.apply(s);
2115 }
2116
2117 }
2118
2119 } else if (s.instanceCount < 2) {
2120
2121 // HTML5 single-instance case
2122
2123 start_html5_timer();
2124
2125 a = s._setup_html5();
2126
2127 s.setPosition(s._iO.position);
2128
2129 a.play();
2130
2131 } else {
2132
2133 // HTML5 multi-shot case
2134
2135 sm2._wD(s.id + ': Cloning Audio() for instance #' + s.instanceCount + '...');
2136
2137 audioClone = new Audio(s._iO.url);
2138
2139 onended = function() {
2140 event.remove(audioClone, 'ended', onended);
2141 s._onfinish(s);
2142 // cleanup
2143 html5Unload(audioClone);
2144 audioClone = null;
2145 };
2146
2147 oncanplay = function() {
2148 event.remove(audioClone, 'canplay', oncanplay);
2149 try {
2150 audioClone.currentTime = s._iO.position / msecScale;
2151 } catch(err) {
2152 complain(s.id + ': multiShot play() failed to apply position of ' + (s._iO.position / msecScale));
2153 }
2154 audioClone.play();
2155 };
2156
2157 event.add(audioClone, 'ended', onended);
2158
2159 // apply volume to clones, too
2160 if (s._iO.volume !== _undefined) {
2161 audioClone.volume = Math.max(0, Math.min(1, s._iO.volume / 100));
2162 }
2163
2164 // playing multiple muted sounds? if you do this, you're weird ;) - but let's cover it.
2165 if (s.muted) {
2166 audioClone.muted = true;
2167 }
2168
2169 if (s._iO.position) {
2170 // HTML5 audio can't seek before onplay() event has fired.
2171 // wait for canplay, then seek to position and start playback.
2172 event.add(audioClone, 'canplay', oncanplay);
2173 } else {
2174 // begin playback at currentTime: 0
2175 audioClone.play();
2176 }
2177
2178 }
2179
2180 }
2181
2182 return s;
2183
2184 };
2185
2186 // just for convenience
2187 this.start = this.play;
2188
2189 /**
2190 * Stops playing a sound (and optionally, all sounds)
2191 *
2192 * @param {boolean} bAll Optional: Whether to stop all sounds
2193 * @return {SMSound} The SMSound object
2194 */
2195
2196 this.stop = function(bAll) {
2197
2198 var instanceOptions = s._iO,
2199 originalPosition;
2200
2201 if (s.playState === 1) {
2202
2203 sm2._wD(s.id + ': stop()');
2204
2205 s._onbufferchange(0);
2206 s._resetOnPosition(0);
2207 s.paused = false;
2208
2209 if (!s.isHTML5) {
2210 s.playState = 0;
2211 }
2212
2213 // remove onPosition listeners, if any
2214 detachOnPosition();
2215
2216 // and "to" position, if set
2217 if (instanceOptions.to) {
2218 s.clearOnPosition(instanceOptions.to);
2219 }
2220
2221 if (!s.isHTML5) {
2222
2223 flash._stop(s.id, bAll);
2224
2225 // hack for netStream: just unload
2226 if (instanceOptions.serverURL) {
2227 s.unload();
2228 }
2229
2230 } else if (s._a) {
2231
2232 originalPosition = s.position;
2233
2234 // act like Flash, though
2235 s.setPosition(0);
2236
2237 // hack: reflect old position for onstop() (also like Flash)
2238 s.position = originalPosition;
2239
2240 // html5 has no stop()
2241 // NOTE: pausing means iOS requires interaction to resume.
2242 s._a.pause();
2243
2244 s.playState = 0;
2245
2246 // and update UI
2247 s._onTimer();
2248
2249 stop_html5_timer();
2250
2251 }
2252
2253 s.instanceCount = 0;
2254 s._iO = {};
2255
2256 if (instanceOptions.onstop) {
2257 instanceOptions.onstop.apply(s);
2258 }
2259
2260 }
2261
2262 return s;
2263
2264 };
2265
2266 /**
2267 * Undocumented/internal: Sets autoPlay for RTMP.
2268 *
2269 * @param {boolean} autoPlay state
2270 */
2271
2272 this.setAutoPlay = function(autoPlay) {
2273
2274 sm2._wD(s.id + ': Autoplay turned ' + (autoPlay ? 'on' : 'off'));
2275 s._iO.autoPlay = autoPlay;
2276
2277 if (!s.isHTML5) {
2278 flash._setAutoPlay(s.id, autoPlay);
2279 if (autoPlay) {
2280 // only increment the instanceCount if the sound isn't loaded (TODO: verify RTMP)
2281 if (!s.instanceCount && s.readyState === 1) {
2282 s.instanceCount++;
2283 sm2._wD(s.id + ': Incremented instance count to ' + s.instanceCount);
2284 }
2285 }
2286 }
2287
2288 };
2289
2290 /**
2291 * Undocumented/internal: Returns the autoPlay boolean.
2292 *
2293 * @return {boolean} The current autoPlay value
2294 */
2295
2296 this.getAutoPlay = function() {
2297
2298 return s._iO.autoPlay;
2299
2300 };
2301
2302 /**
2303 * Sets the playback rate of a sound (HTML5-only.)
2304 *
2305 * @param {number} playbackRate (+/-)
2306 * @return {SMSound} The SMSound object
2307 */
2308
2309 this.setPlaybackRate = function(playbackRate) {
2310
2311 // Per Mozilla, limit acceptable values to prevent playback from stopping (unless allowOverride is truthy.)
2312 // https://developer.mozilla.org/en-US/Apps/Build/Audio_and_video_delivery/WebAudio_playbackRate_explained
2313 var normalizedRate = Math.max(0.5, Math.min(4, playbackRate));
2314
2315 // <d>
2316 if (normalizedRate !== playbackRate) {
2317 sm2._wD(s.id + ': setPlaybackRate(' + playbackRate + '): limiting rate to ' + normalizedRate, 2);
2318 }
2319 // </d>
2320
2321 if (s.isHTML5) {
2322 try {
2323 s._iO.playbackRate = normalizedRate;
2324 s._a.playbackRate = normalizedRate;
2325 } catch(e) {
2326 sm2._wD(s.id + ': setPlaybackRate(' + normalizedRate + ') failed: ' + e.message, 2);
2327 }
2328 }
2329
2330 return s;
2331
2332 };
2333
2334 /**
2335 * Sets the position of a sound.
2336 *
2337 * @param {number} nMsecOffset Position (milliseconds)
2338 * @return {SMSound} The SMSound object
2339 */
2340
2341 this.setPosition = function(nMsecOffset) {
2342
2343 if (nMsecOffset === _undefined) {
2344 nMsecOffset = 0;
2345 }
2346
2347 var position, position1K,
2348 // Use the duration from the instance options, if we don't have a track duration yet.
2349 // position >= 0 and <= current available (loaded) duration
2350 offset = (s.isHTML5 ? Math.max(nMsecOffset, 0) : Math.min(s.duration || s._iO.duration, Math.max(nMsecOffset, 0)));
2351
2352 s.position = offset;
2353 position1K = s.position / msecScale;
2354 s._resetOnPosition(s.position);
2355 s._iO.position = offset;
2356
2357 if (!s.isHTML5) {
2358
2359 position = (fV === 9 ? s.position : position1K);
2360
2361 if (s.readyState && s.readyState !== 2) {
2362 // if paused or not playing, will not resume (by playing)
2363 flash._setPosition(s.id, position, (s.paused || !s.playState), s._iO.multiShot);
2364 }
2365
2366 } else if (s._a) {
2367
2368 // Set the position in the canplay handler if the sound is not ready yet
2369 if (s._html5_canplay) {
2370
2371 if (s._a.currentTime.toFixed(3) !== position1K.toFixed(3)) {
2372
2373 /**
2374 * DOM/JS errors/exceptions to watch out for:
2375 * if seek is beyond (loaded?) position, "DOM exception 11"
2376 * "INDEX_SIZE_ERR": DOM exception 1
2377 */
2378 sm2._wD(s.id + ': setPosition(' + position1K + ')');
2379
2380 try {
2381 s._a.currentTime = position1K;
2382 if (s.playState === 0 || s.paused) {
2383 // allow seek without auto-play/resume
2384 s._a.pause();
2385 }
2386 } catch(e) {
2387 sm2._wD(s.id + ': setPosition(' + position1K + ') failed: ' + e.message, 2);
2388 }
2389
2390 }
2391
2392 } else if (position1K) {
2393
2394 // warn on non-zero seek attempts
2395 sm2._wD(s.id + ': setPosition(' + position1K + '): Cannot seek yet, sound not ready', 2);
2396 return s;
2397
2398 }
2399
2400 if (s.paused) {
2401
2402 // if paused, refresh UI right away by forcing update
2403 s._onTimer(true);
2404
2405 }
2406
2407 }
2408
2409 return s;
2410
2411 };
2412
2413 /**
2414 * Pauses sound playback.
2415 *
2416 * @return {SMSound} The SMSound object
2417 */
2418
2419 this.pause = function(_bCallFlash) {
2420
2421 if (s.paused || (s.playState === 0 && s.readyState !== 1)) return s;
2422
2423 sm2._wD(s.id + ': pause()');
2424 s.paused = true;
2425
2426 if (!s.isHTML5) {
2427 if (_bCallFlash || _bCallFlash === _undefined) {
2428 flash._pause(s.id, s._iO.multiShot);
2429 }
2430 } else {
2431 s._setup_html5().pause();
2432 stop_html5_timer();
2433 }
2434
2435 if (s._iO.onpause) {
2436 s._iO.onpause.apply(s);
2437 }
2438
2439 return s;
2440
2441 };
2442
2443 /**
2444 * Resumes sound playback.
2445 *
2446 * @return {SMSound} The SMSound object
2447 */
2448
2449 /**
2450 * When auto-loaded streams pause on buffer full they have a playState of 0.
2451 * We need to make sure that the playState is set to 1 when these streams "resume".
2452 * When a paused stream is resumed, we need to trigger the onplay() callback if it
2453 * hasn't been called already. In this case since the sound is being played for the
2454 * first time, I think it's more appropriate to call onplay() rather than onresume().
2455 */
2456
2457 this.resume = function() {
2458
2459 var instanceOptions = s._iO;
2460
2461 if (!s.paused) return s;
2462
2463 sm2._wD(s.id + ': resume()');
2464 s.paused = false;
2465 s.playState = 1;
2466
2467 if (!s.isHTML5) {
2468
2469 if (instanceOptions.isMovieStar && !instanceOptions.serverURL) {
2470 // Bizarre Webkit bug (Chrome reported via 8tracks.com dudes): AAC content paused for 30+ seconds(?) will not resume without a reposition.
2471 s.setPosition(s.position);
2472 }
2473
2474 // flash method is toggle-based (pause/resume)
2475 flash._pause(s.id, instanceOptions.multiShot);
2476
2477 } else {
2478
2479 s._setup_html5().play();
2480 start_html5_timer();
2481
2482 }
2483
2484 if (!onplay_called && instanceOptions.onplay) {
2485
2486 instanceOptions.onplay.apply(s);
2487 onplay_called = true;
2488
2489 } else if (instanceOptions.onresume) {
2490
2491 instanceOptions.onresume.apply(s);
2492
2493 }
2494
2495 return s;
2496
2497 };
2498
2499 /**
2500 * Toggles sound playback.
2501 *
2502 * @return {SMSound} The SMSound object
2503 */
2504
2505 this.togglePause = function() {
2506
2507 sm2._wD(s.id + ': togglePause()');
2508
2509 if (s.playState === 0) {
2510 s.play({
2511 position: (fV === 9 && !s.isHTML5 ? s.position : s.position / msecScale)
2512 });
2513 return s;
2514 }
2515
2516 if (s.paused) {
2517 s.resume();
2518 } else {
2519 s.pause();
2520 }
2521
2522 return s;
2523
2524 };
2525
2526 /**
2527 * Sets the panning (L-R) effect.
2528 *
2529 * @param {number} nPan The pan value (-100 to 100)
2530 * @return {SMSound} The SMSound object
2531 */
2532
2533 this.setPan = function(nPan, bInstanceOnly) {
2534
2535 if (nPan === _undefined) {
2536 nPan = 0;
2537 }
2538
2539 if (bInstanceOnly === _undefined) {
2540 bInstanceOnly = false;
2541 }
2542
2543 if (!s.isHTML5) {
2544 flash._setPan(s.id, nPan);
2545 } // else { no HTML5 pan? }
2546
2547 s._iO.pan = nPan;
2548
2549 if (!bInstanceOnly) {
2550 s.pan = nPan;
2551 s.options.pan = nPan;
2552 }
2553
2554 return s;
2555
2556 };
2557
2558 /**
2559 * Sets the volume.
2560 *
2561 * @param {number} nVol The volume value (0 to 100)
2562 * @return {SMSound} The SMSound object
2563 */
2564
2565 this.setVolume = function(nVol, _bInstanceOnly) {
2566
2567 /**
2568 * Note: Setting volume has no effect on iOS "special snowflake" devices.
2569 * Hardware volume control overrides software, and volume
2570 * will always return 1 per Apple docs. (iOS 4 + 5.)
2571 * http://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingSoundtoCanvasAnimations/AddingSoundtoCanvasAnimations.html
2572 */
2573
2574 if (nVol === _undefined) {
2575 nVol = 100;
2576 }
2577
2578 if (_bInstanceOnly === _undefined) {
2579 _bInstanceOnly = false;
2580 }
2581
2582 if (!s.isHTML5) {
2583
2584 flash._setVolume(s.id, (sm2.muted && !s.muted) || s.muted ? 0 : nVol);
2585
2586 } else if (s._a) {
2587
2588 if (sm2.muted && !s.muted) {
2589 s.muted = true;
2590 s._a.muted = true;
2591 }
2592
2593 // valid range for native HTML5 Audio(): 0-1
2594 s._a.volume = Math.max(0, Math.min(1, nVol / 100));
2595
2596 }
2597
2598 s._iO.volume = nVol;
2599
2600 if (!_bInstanceOnly) {
2601 s.volume = nVol;
2602 s.options.volume = nVol;
2603 }
2604
2605 return s;
2606
2607 };
2608
2609 /**
2610 * Mutes the sound.
2611 *
2612 * @return {SMSound} The SMSound object
2613 */
2614
2615 this.mute = function() {
2616
2617 s.muted = true;
2618
2619 if (!s.isHTML5) {
2620 flash._setVolume(s.id, 0);
2621 } else if (s._a) {
2622 s._a.muted = true;
2623 }
2624
2625 return s;
2626
2627 };
2628
2629 /**
2630 * Unmutes the sound.
2631 *
2632 * @return {SMSound} The SMSound object
2633 */
2634
2635 this.unmute = function() {
2636
2637 s.muted = false;
2638 var hasIO = (s._iO.volume !== _undefined);
2639
2640 if (!s.isHTML5) {
2641 flash._setVolume(s.id, hasIO ? s._iO.volume : s.options.volume);
2642 } else if (s._a) {
2643 s._a.muted = false;
2644 }
2645
2646 return s;
2647
2648 };
2649
2650 /**
2651 * Toggles the muted state of a sound.
2652 *
2653 * @return {SMSound} The SMSound object
2654 */
2655
2656 this.toggleMute = function() {
2657
2658 return (s.muted ? s.unmute() : s.mute());
2659
2660 };
2661
2662 /**
2663 * Registers a callback to be fired when a sound reaches a given position during playback.
2664 *
2665 * @param {number} nPosition The position to watch for
2666 * @param {function} oMethod The relevant callback to fire
2667 * @param {object} oScope Optional: The scope to apply the callback to
2668 * @return {SMSound} The SMSound object
2669 */
2670
2671 this.onPosition = function(nPosition, oMethod, oScope) {
2672
2673 // TODO: basic dupe checking?
2674
2675 onPositionItems.push({
2676 position: parseInt(nPosition, 10),
2677 method: oMethod,
2678 scope: (oScope !== _undefined ? oScope : s),
2679 fired: false
2680 });
2681
2682 return s;
2683
2684 };
2685
2686 // legacy/backwards-compability: lower-case method name
2687 this.onposition = this.onPosition;
2688
2689 /**
2690 * Removes registered callback(s) from a sound, by position and/or callback.
2691 *
2692 * @param {number} nPosition The position to clear callback(s) for
2693 * @param {function} oMethod Optional: Identify one callback to be removed when multiple listeners exist for one position
2694 * @return {SMSound} The SMSound object
2695 */
2696
2697 this.clearOnPosition = function(nPosition, oMethod) {
2698
2699 var i;
2700
2701 nPosition = parseInt(nPosition, 10);
2702
2703 if (isNaN(nPosition)) {
2704 // safety check
2705 return;
2706 }
2707
2708 for (i = 0; i < onPositionItems.length; i++) {
2709
2710 if (nPosition === onPositionItems[i].position) {
2711 // remove this item if no method was specified, or, if the method matches
2712
2713 if (!oMethod || (oMethod === onPositionItems[i].method)) {
2714
2715 if (onPositionItems[i].fired) {
2716 // decrement "fired" counter, too
2717 onPositionFired--;
2718 }
2719
2720 onPositionItems.splice(i, 1);
2721
2722 }
2723
2724 }
2725
2726 }
2727
2728 };
2729
2730 this._processOnPosition = function() {
2731
2732 var i, item, j = onPositionItems.length;
2733
2734 if (!j || !s.playState || onPositionFired >= j) return false;
2735
2736 for (i = j - 1; i >= 0; i--) {
2737
2738 item = onPositionItems[i];
2739
2740 if (!item.fired && s.position >= item.position) {
2741
2742 item.fired = true;
2743 onPositionFired++;
2744 item.method.apply(item.scope, [item.position]);
2745
2746 // reset j -- onPositionItems.length can be changed in the item callback above... occasionally breaking the loop.
2747 j = onPositionItems.length;
2748
2749 }
2750
2751 }
2752
2753 return true;
2754
2755 };
2756
2757 this._resetOnPosition = function(nPosition) {
2758
2759 // reset "fired" for items interested in this position
2760 var i, item, j = onPositionItems.length;
2761
2762 if (!j) return false;
2763
2764 for (i = j - 1; i >= 0; i--) {
2765
2766 item = onPositionItems[i];
2767
2768 if (item.fired && nPosition <= item.position) {
2769 item.fired = false;
2770 onPositionFired--;
2771 }
2772
2773 }
2774
2775 return true;
2776
2777 };
2778
2779 /**
2780 * SMSound() private internals
2781 * --------------------------------
2782 */
2783
2784 applyFromTo = function() {
2785
2786 var instanceOptions = s._iO,
2787 f = instanceOptions.from,
2788 t = instanceOptions.to,
2789 start, end;
2790
2791 end = function() {
2792
2793 // end has been reached.
2794 sm2._wD(s.id + ': "To" time of ' + t + ' reached.');
2795
2796 // detach listener
2797 s.clearOnPosition(t, end);
2798
2799 // stop should clear this, too
2800 s.stop();
2801
2802 };
2803
2804 start = function() {
2805
2806 sm2._wD(s.id + ': Playing "from" ' + f);
2807
2808 // add listener for end
2809 if (t !== null && !isNaN(t)) {
2810 s.onPosition(t, end);
2811 }
2812
2813 };
2814
2815 if (f !== null && !isNaN(f)) {
2816
2817 // apply to instance options, guaranteeing correct start position.
2818 instanceOptions.position = f;
2819
2820 // multiShot timing can't be tracked, so prevent that.
2821 instanceOptions.multiShot = false;
2822
2823 start();
2824
2825 }
2826
2827 // return updated instanceOptions including starting position
2828 return instanceOptions;
2829
2830 };
2831
2832 attachOnPosition = function() {
2833
2834 var item,
2835 op = s._iO.onposition;
2836
2837 // attach onposition things, if any, now.
2838
2839 if (op) {
2840
2841 for (item in op) {
2842 if (op.hasOwnProperty(item)) {
2843 s.onPosition(parseInt(item, 10), op[item]);
2844 }
2845 }
2846
2847 }
2848
2849 };
2850
2851 detachOnPosition = function() {
2852
2853 var item,
2854 op = s._iO.onposition;
2855
2856 // detach any onposition()-style listeners.
2857
2858 if (op) {
2859
2860 for (item in op) {
2861 if (op.hasOwnProperty(item)) {
2862 s.clearOnPosition(parseInt(item, 10));
2863 }
2864 }
2865
2866 }
2867
2868 };
2869
2870 start_html5_timer = function() {
2871
2872 if (s.isHTML5) {
2873 startTimer(s);
2874 }
2875
2876 };
2877
2878 stop_html5_timer = function() {
2879
2880 if (s.isHTML5) {
2881 stopTimer(s);
2882 }
2883
2884 };
2885
2886 resetProperties = function(retainPosition) {
2887
2888 if (!retainPosition) {
2889 onPositionItems = [];
2890 onPositionFired = 0;
2891 }
2892
2893 onplay_called = false;
2894
2895 s._hasTimer = null;
2896 s._a = null;
2897 s._html5_canplay = false;
2898 s.bytesLoaded = null;
2899 s.bytesTotal = null;
2900 s.duration = (s._iO && s._iO.duration ? s._iO.duration : null);
2901 s.durationEstimate = null;
2902 s.buffered = [];
2903
2904 // legacy: 1D array
2905 s.eqData = [];
2906
2907 s.eqData.left = [];
2908 s.eqData.right = [];
2909
2910 s.failures = 0;
2911 s.isBuffering = false;
2912 s.instanceOptions = {};
2913 s.instanceCount = 0;
2914 s.loaded = false;
2915 s.metadata = {};
2916
2917 // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
2918 s.readyState = 0;
2919
2920 s.muted = false;
2921 s.paused = false;
2922
2923 s.peakData = {
2924 left: 0,
2925 right: 0
2926 };
2927
2928 s.waveformData = {
2929 left: [],
2930 right: []
2931 };
2932
2933 s.playState = 0;
2934 s.position = null;
2935
2936 s.id3 = {};
2937
2938 };
2939
2940 resetProperties();
2941
2942 /**
2943 * Pseudo-private SMSound internals
2944 * --------------------------------
2945 */
2946
2947 this._onTimer = function(bForce) {
2948
2949 /**
2950 * HTML5-only _whileplaying() etc.
2951 * called from both HTML5 native events, and polling/interval-based timers
2952 * mimics flash and fires only when time/duration change, so as to be polling-friendly
2953 */
2954
2955 var duration, isNew = false, time, x = {};
2956
2957 if (s._hasTimer || bForce) {
2958
2959 // TODO: May not need to track readyState (1 = loading)
2960
2961 if (s._a && (bForce || ((s.playState > 0 || s.readyState === 1) && !s.paused))) {
2962
2963 duration = s._get_html5_duration();
2964
2965 if (duration !== lastHTML5State.duration) {
2966
2967 lastHTML5State.duration = duration;
2968 s.duration = duration;
2969 isNew = true;
2970
2971 }
2972
2973 // TODO: investigate why this goes wack if not set/re-set each time.
2974 s.durationEstimate = s.duration;
2975
2976 time = (s._a.currentTime * msecScale || 0);
2977
2978 if (time !== lastHTML5State.time) {
2979
2980 lastHTML5State.time = time;
2981 isNew = true;
2982
2983 }
2984
2985 if (isNew || bForce) {
2986
2987 s._whileplaying(time, x, x, x, x);
2988
2989 }
2990
2991 }/* else {
2992
2993 // sm2._wD('_onTimer: Warn for "'+s.id+'": '+(!s._a?'Could not find element. ':'')+(s.playState === 0?'playState bad, 0?':'playState = '+s.playState+', OK'));
2994
2995 return false;
2996
2997 }*/
2998
2999 }
3000
3001 return isNew;
3002
3003 };
3004
3005 this._get_html5_duration = function() {
3006
3007 var instanceOptions = s._iO,
3008 // if audio object exists, use its duration - else, instance option duration (if provided - it's a hack, really, and should be retired) OR null
3009 d = (s._a && s._a.duration ? s._a.duration * msecScale : (instanceOptions && instanceOptions.duration ? instanceOptions.duration : null)),
3010 result = (d && !isNaN(d) && d !== Infinity ? d : null);
3011
3012 return result;
3013
3014 };
3015
3016 this._apply_loop = function(a, nLoops) {
3017
3018 /**
3019 * boolean instead of "loop", for webkit? - spec says string. http://www.w3.org/TR/html-markup/audio.html#audio.attrs.loop
3020 * note that loop is either off or infinite under HTML5, unlike Flash which allows arbitrary loop counts to be specified.
3021 */
3022
3023 // <d>
3024 if (!a.loop && nLoops > 1) {
3025 sm2._wD('Note: Native HTML5 looping is infinite.', 1);
3026 }
3027 // </d>
3028
3029 a.loop = (nLoops > 1 ? 'loop' : '');
3030
3031 };
3032
3033 this._setup_html5 = function(options) {
3034
3035 var instanceOptions = mixin(s._iO, options),
3036 a = useGlobalHTML5Audio ? globalHTML5Audio : s._a,
3037 dURL = decodeURI(instanceOptions.url),
3038 sameURL;
3039
3040 /**
3041 * "First things first, I, Poppa..." (reset the previous state of the old sound, if playing)
3042 * Fixes case with devices that can only play one sound at a time
3043 * Otherwise, other sounds in mid-play will be terminated without warning and in a stuck state
3044 */
3045
3046 if (useGlobalHTML5Audio) {
3047
3048 if (dURL === decodeURI(lastGlobalHTML5URL)) {
3049 // global HTML5 audio: re-use of URL
3050 sameURL = true;
3051 }
3052
3053 } else if (dURL === decodeURI(lastURL)) {
3054
3055 // options URL is the same as the "last" URL, and we used (loaded) it
3056 sameURL = true;
3057
3058 }
3059
3060 if (a) {
3061
3062 if (a._s) {
3063
3064 if (useGlobalHTML5Audio) {
3065
3066 if (a._s && a._s.playState && !sameURL) {
3067
3068 // global HTML5 audio case, and loading a new URL. stop the currently-playing one.
3069 a._s.stop();
3070
3071 }
3072
3073 } else if (!useGlobalHTML5Audio && dURL === decodeURI(lastURL)) {
3074
3075 // non-global HTML5 reuse case: same url, ignore request
3076 s._apply_loop(a, instanceOptions.loops);
3077
3078 return a;
3079
3080 }
3081
3082 }
3083
3084 if (!sameURL) {
3085
3086 // don't retain onPosition() stuff with new URLs.
3087
3088 if (lastURL) {
3089 resetProperties(false);
3090 }
3091
3092 // assign new HTML5 URL
3093
3094 a.src = instanceOptions.url;
3095
3096 s.url = instanceOptions.url;
3097
3098 lastURL = instanceOptions.url;
3099
3100 lastGlobalHTML5URL = instanceOptions.url;
3101
3102 a._called_load = false;
3103
3104 }
3105
3106 } else {
3107
3108 if (instanceOptions.autoLoad || instanceOptions.autoPlay) {
3109
3110 s._a = new Audio(instanceOptions.url);
3111 s._a.load();
3112
3113 } else {
3114
3115 // null for stupid Opera 9.64 case
3116 s._a = (isOpera && opera.version() < 10 ? new Audio(null) : new Audio());
3117
3118 }
3119
3120 // assign local reference
3121 a = s._a;
3122
3123 a._called_load = false;
3124
3125 if (useGlobalHTML5Audio) {
3126
3127 globalHTML5Audio = a;
3128
3129 }
3130
3131 }
3132
3133 s.isHTML5 = true;
3134
3135 // store a ref on the track
3136 s._a = a;
3137
3138 // store a ref on the audio
3139 a._s = s;
3140
3141 add_html5_events();
3142
3143 s._apply_loop(a, instanceOptions.loops);
3144
3145 if (instanceOptions.autoLoad || instanceOptions.autoPlay) {
3146
3147 s.load();
3148
3149 } else {
3150
3151 // early HTML5 implementation (non-standard)
3152 a.autobuffer = false;
3153
3154 // standard ('none' is also an option.)
3155 a.preload = 'auto';
3156
3157 }
3158
3159 return a;
3160
3161 };
3162
3163 add_html5_events = function() {
3164
3165 if (s._a._added_events) return false;
3166
3167 var f;
3168
3169 function add(oEvt, oFn, bCapture) {
3170 return s._a ? s._a.addEventListener(oEvt, oFn, bCapture || false) : null;
3171 }
3172
3173 s._a._added_events = true;
3174
3175 for (f in html5_events) {
3176 if (html5_events.hasOwnProperty(f)) {
3177 add(f, html5_events[f]);
3178 }
3179 }
3180
3181 return true;
3182
3183 };
3184
3185 remove_html5_events = function() {
3186
3187 // Remove event listeners
3188
3189 var f;
3190
3191 function remove(oEvt, oFn, bCapture) {
3192 return (s._a ? s._a.removeEventListener(oEvt, oFn, bCapture || false) : null);
3193 }
3194
3195 sm2._wD(s.id + ': Removing event listeners');
3196 s._a._added_events = false;
3197
3198 for (f in html5_events) {
3199 if (html5_events.hasOwnProperty(f)) {
3200 remove(f, html5_events[f]);
3201 }
3202 }
3203
3204 };
3205
3206 /**
3207 * Pseudo-private event internals
3208 * ------------------------------
3209 */
3210
3211 this._onload = function(nSuccess) {
3212
3213 var fN,
3214 // check for duration to prevent false positives from flash 8 when loading from cache.
3215 loadOK = !!nSuccess || (!s.isHTML5 && fV === 8 && s.duration);
3216
3217 // <d>
3218 fN = s.id + ': ';
3219 sm2._wD(fN + (loadOK ? 'onload()' : 'Failed to load / invalid sound?' + (!s.duration ? ' Zero-length duration reported.' : ' -') + ' (' + s.url + ')'), (loadOK ? 1 : 2));
3220
3221 if (!loadOK && !s.isHTML5) {
3222 if (sm2.sandbox.noRemote === true) {
3223 sm2._wD(fN + str('noNet'), 1);
3224 }
3225 if (sm2.sandbox.noLocal === true) {
3226 sm2._wD(fN + str('noLocal'), 1);
3227 }
3228 }
3229 // </d>
3230
3231 s.loaded = loadOK;
3232 s.readyState = (loadOK ? 3 : 2);
3233 s._onbufferchange(0);
3234
3235 if (!loadOK && !s.isHTML5) {
3236 // note: no error code from Flash.
3237 s._onerror();
3238 }
3239
3240 if (s._iO.onload) {
3241 wrapCallback(s, function() {
3242 s._iO.onload.apply(s, [loadOK]);
3243 });
3244 }
3245
3246 return true;
3247
3248 };
3249
3250 this._onerror = function(errorCode, description) {
3251
3252 // https://html.spec.whatwg.org/multipage/embedded-content.html#error-codes
3253 if (s._iO.onerror) {
3254 wrapCallback(s, function() {
3255 s._iO.onerror.apply(s, [errorCode, description]);
3256 });
3257 }
3258
3259 };
3260
3261 this._onbufferchange = function(nIsBuffering) {
3262
3263 // ignore if not playing
3264 if (s.playState === 0) return false;
3265
3266 if ((nIsBuffering && s.isBuffering) || (!nIsBuffering && !s.isBuffering)) return false;
3267
3268 s.isBuffering = (nIsBuffering === 1);
3269
3270 if (s._iO.onbufferchange) {
3271 sm2._wD(s.id + ': Buffer state change: ' + nIsBuffering);
3272 s._iO.onbufferchange.apply(s, [nIsBuffering]);
3273 }
3274
3275 return true;
3276
3277 };
3278
3279 /**
3280 * Playback may have stopped due to buffering, or related reason.
3281 * This state can be encountered on iOS < 6 when auto-play is blocked.
3282 */
3283
3284 this._onsuspend = function() {
3285
3286 if (s._iO.onsuspend) {
3287 sm2._wD(s.id + ': Playback suspended');
3288 s._iO.onsuspend.apply(s);
3289 }
3290
3291 return true;
3292
3293 };
3294
3295 /**
3296 * flash 9/movieStar + RTMP-only method, should fire only once at most
3297 * at this point we just recreate failed sounds rather than trying to reconnect
3298 */
3299
3300 this._onfailure = function(msg, level, code) {
3301
3302 s.failures++;
3303 sm2._wD(s.id + ': Failure (' + s.failures + '): ' + msg);
3304
3305 if (s._iO.onfailure && s.failures === 1) {
3306 s._iO.onfailure(msg, level, code);
3307 } else {
3308 sm2._wD(s.id + ': Ignoring failure');
3309 }
3310
3311 };
3312
3313 /**
3314 * flash 9/movieStar + RTMP-only method for unhandled warnings/exceptions from Flash
3315 * e.g., RTMP "method missing" warning (non-fatal) for getStreamLength on server
3316 */
3317
3318 this._onwarning = function(msg, level, code) {
3319
3320 if (s._iO.onwarning) {
3321 s._iO.onwarning(msg, level, code);
3322 }
3323
3324 };
3325
3326 this._onfinish = function() {
3327
3328 // store local copy before it gets trashed...
3329 var io_onfinish = s._iO.onfinish;
3330
3331 s._onbufferchange(0);
3332 s._resetOnPosition(0);
3333
3334 // reset some state items
3335 if (s.instanceCount) {
3336
3337 s.instanceCount--;
3338
3339 if (!s.instanceCount) {
3340
3341 // remove onPosition listeners, if any
3342 detachOnPosition();
3343
3344 // reset instance options
3345 s.playState = 0;
3346 s.paused = false;
3347 s.instanceCount = 0;
3348 s.instanceOptions = {};
3349 s._iO = {};
3350 stop_html5_timer();
3351
3352 // reset position, too
3353 if (s.isHTML5) {
3354 s.position = 0;
3355 }
3356
3357 }
3358
3359 if (!s.instanceCount || s._iO.multiShotEvents) {
3360 // fire onfinish for last, or every instance
3361 if (io_onfinish) {
3362 sm2._wD(s.id + ': onfinish()');
3363 wrapCallback(s, function() {
3364 io_onfinish.apply(s);
3365 });
3366 }
3367 }
3368
3369 }
3370
3371 };
3372
3373 this._whileloading = function(nBytesLoaded, nBytesTotal, nDuration, nBufferLength) {
3374
3375 var instanceOptions = s._iO;
3376
3377 s.bytesLoaded = nBytesLoaded;
3378 s.bytesTotal = nBytesTotal;
3379 s.duration = Math.floor(nDuration);
3380 s.bufferLength = nBufferLength;
3381
3382 if (!s.isHTML5 && !instanceOptions.isMovieStar) {
3383
3384 if (instanceOptions.duration) {
3385 // use duration from options, if specified and larger. nobody should be specifying duration in options, actually, and it should be retired.
3386 s.durationEstimate = (s.duration > instanceOptions.duration) ? s.duration : instanceOptions.duration;
3387 } else {
3388 s.durationEstimate = parseInt((s.bytesTotal / s.bytesLoaded) * s.duration, 10);
3389 }
3390
3391 } else {
3392
3393 s.durationEstimate = s.duration;
3394
3395 }
3396
3397 // for flash, reflect sequential-load-style buffering
3398 if (!s.isHTML5) {
3399 s.buffered = [{
3400 start: 0,
3401 end: s.duration
3402 }];
3403 }
3404
3405 // allow whileloading to fire even if "load" fired under HTML5, due to HTTP range/partials
3406 if ((s.readyState !== 3 || s.isHTML5) && instanceOptions.whileloading) {
3407 instanceOptions.whileloading.apply(s);
3408 }
3409
3410 };
3411
3412 this._whileplaying = function(nPosition, oPeakData, oWaveformDataLeft, oWaveformDataRight, oEQData) {
3413
3414 var instanceOptions = s._iO,
3415 eqLeft;
3416
3417 // flash safety net
3418 if (isNaN(nPosition) || nPosition === null) return false;
3419
3420 // 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.
3421 s.position = Math.max(0, nPosition);
3422
3423 s._processOnPosition();
3424
3425 if (!s.isHTML5 && fV > 8) {
3426
3427 if (instanceOptions.usePeakData && oPeakData !== _undefined && oPeakData) {
3428 s.peakData = {
3429 left: oPeakData.leftPeak,
3430 right: oPeakData.rightPeak
3431 };
3432 }
3433
3434 if (instanceOptions.useWaveformData && oWaveformDataLeft !== _undefined && oWaveformDataLeft) {
3435 s.waveformData = {
3436 left: oWaveformDataLeft.split(','),
3437 right: oWaveformDataRight.split(',')
3438 };
3439 }
3440
3441 if (instanceOptions.useEQData) {
3442 if (oEQData !== _undefined && oEQData && oEQData.leftEQ) {
3443 eqLeft = oEQData.leftEQ.split(',');
3444 s.eqData = eqLeft;
3445 s.eqData.left = eqLeft;
3446 if (oEQData.rightEQ !== _undefined && oEQData.rightEQ) {
3447 s.eqData.right = oEQData.rightEQ.split(',');
3448 }
3449 }
3450 }
3451
3452 }
3453
3454 if (s.playState === 1) {
3455
3456 // special case/hack: ensure buffering is false if loading from cache (and not yet started)
3457 if (!s.isHTML5 && fV === 8 && !s.position && s.isBuffering) {
3458 s._onbufferchange(0);
3459 }
3460
3461 if (instanceOptions.whileplaying) {
3462 // flash may call after actual finish
3463 instanceOptions.whileplaying.apply(s);
3464 }
3465
3466 }
3467
3468 return true;
3469
3470 };
3471
3472 this._oncaptiondata = function(oData) {
3473
3474 /**
3475 * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature
3476 *
3477 * @param {object} oData
3478 */
3479
3480 sm2._wD(s.id + ': Caption data received.');
3481
3482 s.captiondata = oData;
3483
3484 if (s._iO.oncaptiondata) {
3485 s._iO.oncaptiondata.apply(s, [oData]);
3486 }
3487
3488 };
3489
3490 this._onmetadata = function(oMDProps, oMDData) {
3491
3492 /**
3493 * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature
3494 * RTMP may include song title, MovieStar content may include encoding info
3495 *
3496 * @param {array} oMDProps (names)
3497 * @param {array} oMDData (values)
3498 */
3499
3500 sm2._wD(s.id + ': Metadata received.');
3501
3502 var oData = {}, i, j;
3503
3504 for (i = 0, j = oMDProps.length; i < j; i++) {
3505 oData[oMDProps[i]] = oMDData[i];
3506 }
3507
3508 s.metadata = oData;
3509
3510 if (s._iO.onmetadata) {
3511 s._iO.onmetadata.call(s, s.metadata);
3512 }
3513
3514 };
3515
3516 this._onid3 = function(oID3Props, oID3Data) {
3517
3518 /**
3519 * internal: flash 8 + flash 9 ID3 feature
3520 * may include artist, song title etc.
3521 *
3522 * @param {array} oID3Props (names)
3523 * @param {array} oID3Data (values)
3524 */
3525
3526 sm2._wD(s.id + ': ID3 data received.');
3527
3528 var oData = [], i, j;
3529
3530 for (i = 0, j = oID3Props.length; i < j; i++) {
3531 oData[oID3Props[i]] = oID3Data[i];
3532 }
3533
3534 s.id3 = mixin(s.id3, oData);
3535
3536 if (s._iO.onid3) {
3537 s._iO.onid3.apply(s);
3538 }
3539
3540 };
3541
3542 // flash/RTMP-only
3543
3544 this._onconnect = function(bSuccess) {
3545
3546 bSuccess = (bSuccess === 1);
3547 sm2._wD(s.id + ': ' + (bSuccess ? 'Connected.' : 'Failed to connect? - ' + s.url), (bSuccess ? 1 : 2));
3548 s.connected = bSuccess;
3549
3550 if (bSuccess) {
3551
3552 s.failures = 0;
3553
3554 if (idCheck(s.id)) {
3555 if (s.getAutoPlay()) {
3556 // only update the play state if auto playing
3557 s.play(_undefined, s.getAutoPlay());
3558 } else if (s._iO.autoLoad) {
3559 s.load();
3560 }
3561 }
3562
3563 if (s._iO.onconnect) {
3564 s._iO.onconnect.apply(s, [bSuccess]);
3565 }
3566
3567 }
3568
3569 };
3570
3571 this._ondataerror = function(sError) {
3572
3573 // flash 9 wave/eq data handler
3574 // hack: called at start, and end from flash at/after onfinish()
3575 if (s.playState > 0) {
3576 sm2._wD(s.id + ': Data error: ' + sError);
3577 if (s._iO.ondataerror) {
3578 s._iO.ondataerror.apply(s);
3579 }
3580 }
3581
3582 };
3583
3584 // <d>
3585 this._debug();
3586 // </d>
3587
3588 }; // SMSound()
3589
3590 /**
3591 * Private SoundManager internals
3592 * ------------------------------
3593 */
3594
3595 getDocument = function() {
3596
3597 return (doc.body || doc.getElementsByTagName('div')[0]);
3598
3599 };
3600
3601 id = function(sID) {
3602
3603 return doc.getElementById(sID);
3604
3605 };
3606
3607 mixin = function(oMain, oAdd) {
3608
3609 // non-destructive merge
3610 var o1 = (oMain || {}), o2, o;
3611
3612 // if unspecified, o2 is the default options object
3613 o2 = (oAdd === _undefined ? sm2.defaultOptions : oAdd);
3614
3615 for (o in o2) {
3616
3617 if (o2.hasOwnProperty(o) && o1[o] === _undefined) {
3618
3619 if (typeof o2[o] !== 'object' || o2[o] === null) {
3620
3621 // assign directly
3622 o1[o] = o2[o];
3623
3624 } else {
3625
3626 // recurse through o2
3627 o1[o] = mixin(o1[o], o2[o]);
3628
3629 }
3630
3631 }
3632
3633 }
3634
3635 return o1;
3636
3637 };
3638
3639 wrapCallback = function(oSound, callback) {
3640
3641 /**
3642 * 03/03/2013: Fix for Flash Player 11.6.602.171 + Flash 8 (flashVersion = 8) SWF issue
3643 * 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).
3644 * 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.
3645 * Fix: setTimeout() to yield, plus safer null / NaN checking on position argument provided to Flash.
3646 * https://getsatisfaction.com/schillmania/topics/recent_chrome_update_seems_to_have_broken_my_sm2_audio_player
3647 */
3648 if (!oSound.isHTML5 && fV === 8) {
3649 window.setTimeout(callback, 0);
3650 } else {
3651 callback();
3652 }
3653
3654 };
3655
3656 // additional soundManager properties that soundManager.setup() will accept
3657
3658 extraOptions = {
3659 onready: 1,
3660 ontimeout: 1,
3661 defaultOptions: 1,
3662 flash9Options: 1,
3663 movieStarOptions: 1
3664 };
3665
3666 assign = function(o, oParent) {
3667
3668 /**
3669 * recursive assignment of properties, soundManager.setup() helper
3670 * allows property assignment based on whitelist
3671 */
3672
3673 var i,
3674 result = true,
3675 hasParent = (oParent !== _undefined),
3676 setupOptions = sm2.setupOptions,
3677 bonusOptions = extraOptions;
3678
3679 // <d>
3680
3681 // if soundManager.setup() called, show accepted parameters.
3682
3683 if (o === _undefined) {
3684
3685 result = [];
3686
3687 for (i in setupOptions) {
3688
3689 if (setupOptions.hasOwnProperty(i)) {
3690 result.push(i);
3691 }
3692
3693 }
3694
3695 for (i in bonusOptions) {
3696
3697 if (bonusOptions.hasOwnProperty(i)) {
3698
3699 if (typeof sm2[i] === 'object') {
3700 result.push(i + ': {...}');
3701 } else if (sm2[i] instanceof Function) {
3702 result.push(i + ': function() {...}');
3703 } else {
3704 result.push(i);
3705 }
3706
3707 }
3708
3709 }
3710
3711 sm2._wD(str('setup', result.join(', ')));
3712
3713 return false;
3714
3715 }
3716
3717 // </d>
3718
3719 for (i in o) {
3720
3721 if (o.hasOwnProperty(i)) {
3722
3723 // if not an {object} we want to recurse through...
3724
3725 if (typeof o[i] !== 'object' || o[i] === null || o[i] instanceof Array || o[i] instanceof RegExp) {
3726
3727 // check "allowed" options
3728
3729 if (hasParent && bonusOptions[oParent] !== _undefined) {
3730
3731 // valid recursive / nested object option, eg., { defaultOptions: { volume: 50 } }
3732 sm2[oParent][i] = o[i];
3733
3734 } else if (setupOptions[i] !== _undefined) {
3735
3736 // special case: assign to setupOptions object, which soundManager property references
3737 sm2.setupOptions[i] = o[i];
3738
3739 // assign directly to soundManager, too
3740 sm2[i] = o[i];
3741
3742 } else if (bonusOptions[i] === _undefined) {
3743
3744 // invalid or disallowed parameter. complain.
3745 complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2);
3746
3747 result = false;
3748
3749 } else if (sm2[i] instanceof Function) {
3750
3751 /**
3752 * valid extraOptions (bonusOptions) parameter.
3753 * is it a method, like onready/ontimeout? call it.
3754 * multiple parameters should be in an array, eg. soundManager.setup({onready: [myHandler, myScope]});
3755 */
3756 sm2[i].apply(sm2, (o[i] instanceof Array ? o[i] : [o[i]]));
3757
3758 } else {
3759
3760 // good old-fashioned direct assignment
3761 sm2[i] = o[i];
3762
3763 }
3764
3765 } else if (bonusOptions[i] === _undefined) {
3766
3767 // recursion case, eg., { defaultOptions: { ... } }
3768
3769 // invalid or disallowed parameter. complain.
3770 complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2);
3771
3772 result = false;
3773
3774 } else {
3775
3776 // recurse through object
3777 return assign(o[i], i);
3778
3779 }
3780
3781 }
3782
3783 }
3784
3785 return result;
3786
3787 };
3788
3789 function preferFlashCheck(kind) {
3790
3791 // whether flash should play a given type
3792 return (sm2.preferFlash && hasFlash && !sm2.ignoreFlash && (sm2.flash[kind] !== _undefined && sm2.flash[kind]));
3793
3794 }
3795
3796 /**
3797 * Internal DOM2-level event helpers
3798 * ---------------------------------
3799 */
3800
3801 event = (function() {
3802
3803 // normalize event methods
3804 var old = (window.attachEvent),
3805 evt = {
3806 add: (old ? 'attachEvent' : 'addEventListener'),
3807 remove: (old ? 'detachEvent' : 'removeEventListener')
3808 };
3809
3810 // normalize "on" event prefix, optional capture argument
3811 function getArgs(oArgs) {
3812
3813 var args = slice.call(oArgs),
3814 len = args.length;
3815
3816 if (old) {
3817 // prefix
3818 args[1] = 'on' + args[1];
3819 if (len > 3) {
3820 // no capture
3821 args.pop();
3822 }
3823 } else if (len === 3) {
3824 args.push(false);
3825 }
3826
3827 return args;
3828
3829 }
3830
3831 function apply(args, sType) {
3832
3833 // normalize and call the event method, with the proper arguments
3834 var element = args.shift(),
3835 method = [evt[sType]];
3836
3837 if (old) {
3838 // old IE can't do apply().
3839 element[method](args[0], args[1]);
3840 } else {
3841 element[method].apply(element, args);
3842 }
3843
3844 }
3845
3846 function add() {
3847 apply(getArgs(arguments), 'add');
3848 }
3849
3850 function remove() {
3851 apply(getArgs(arguments), 'remove');
3852 }
3853
3854 return {
3855 add: add,
3856 remove: remove
3857 };
3858
3859 }());
3860
3861 /**
3862 * Internal HTML5 event handling
3863 * -----------------------------
3864 */
3865
3866 function html5_event(oFn) {
3867
3868 // wrap html5 event handlers so we don't call them on destroyed and/or unloaded sounds
3869
3870 return function(e) {
3871
3872 var s = this._s,
3873 result;
3874
3875 if (!s || !s._a) {
3876 // <d>
3877 if (s && s.id) {
3878 sm2._wD(s.id + ': Ignoring ' + e.type);
3879 } else {
3880 sm2._wD(h5 + 'Ignoring ' + e.type);
3881 }
3882 // </d>
3883 result = null;
3884 } else {
3885 result = oFn.call(this, e);
3886 }
3887
3888 return result;
3889
3890 };
3891
3892 }
3893
3894 html5_events = {
3895
3896 // HTML5 event-name-to-handler map
3897
3898 abort: html5_event(function() {
3899
3900 sm2._wD(this._s.id + ': abort');
3901
3902 }),
3903
3904 // enough has loaded to play
3905
3906 canplay: html5_event(function() {
3907
3908 var s = this._s,
3909 position1K;
3910
3911 if (s._html5_canplay) {
3912 // this event has already fired. ignore.
3913 return;
3914 }
3915
3916 s._html5_canplay = true;
3917 sm2._wD(s.id + ': canplay');
3918 s._onbufferchange(0);
3919
3920 // position according to instance options
3921 position1K = (s._iO.position !== _undefined && !isNaN(s._iO.position) ? s._iO.position / msecScale : null);
3922
3923 // set the position if position was provided before the sound loaded
3924 if (this.currentTime !== position1K) {
3925 sm2._wD(s.id + ': canplay: Setting position to ' + position1K);
3926 try {
3927 this.currentTime = position1K;
3928 } catch(ee) {
3929 sm2._wD(s.id + ': canplay: Setting position of ' + position1K + ' failed: ' + ee.message, 2);
3930 }
3931 }
3932
3933 // hack for HTML5 from/to case
3934 if (s._iO._oncanplay) {
3935 s._iO._oncanplay();
3936 }
3937
3938 }),
3939
3940 canplaythrough: html5_event(function() {
3941
3942 var s = this._s;
3943
3944 if (!s.loaded) {
3945 s._onbufferchange(0);
3946 s._whileloading(s.bytesLoaded, s.bytesTotal, s._get_html5_duration());
3947 s._onload(true);
3948 }
3949
3950 }),
3951
3952 durationchange: html5_event(function() {
3953
3954 // durationchange may fire at various times, probably the safest way to capture accurate/final duration.
3955
3956 var s = this._s,
3957 duration;
3958
3959 duration = s._get_html5_duration();
3960
3961 if (!isNaN(duration) && duration !== s.duration) {
3962
3963 sm2._wD(this._s.id + ': durationchange (' + duration + ')' + (s.duration ? ', previously ' + s.duration : ''));
3964
3965 s.durationEstimate = s.duration = duration;
3966
3967 }
3968
3969 }),
3970
3971 // TODO: Reserved for potential use
3972 /*
3973 emptied: html5_event(function() {
3974
3975 sm2._wD(this._s.id + ': emptied');
3976
3977 }),
3978 */
3979
3980 ended: html5_event(function() {
3981
3982 var s = this._s;
3983
3984 sm2._wD(s.id + ': ended');
3985
3986 s._onfinish();
3987
3988 }),
3989
3990 error: html5_event(function() {
3991
3992 var description = (html5ErrorCodes[this.error.code] || null);
3993 sm2._wD(this._s.id + ': HTML5 error, code ' + this.error.code + (description ? ' (' + description + ')' : ''));
3994 this._s._onload(false);
3995 this._s._onerror(this.error.code, description);
3996
3997 }),
3998
3999 loadeddata: html5_event(function() {
4000
4001 var s = this._s;
4002
4003 sm2._wD(s.id + ': loadeddata');
4004
4005 // safari seems to nicely report progress events, eventually totalling 100%
4006 if (!s._loaded && !isSafari) {
4007 s.duration = s._get_html5_duration();
4008 }
4009
4010 }),
4011
4012 loadedmetadata: html5_event(function() {
4013
4014 sm2._wD(this._s.id + ': loadedmetadata');
4015
4016 }),
4017
4018 loadstart: html5_event(function() {
4019
4020 sm2._wD(this._s.id + ': loadstart');
4021 // assume buffering at first
4022 this._s._onbufferchange(1);
4023
4024 }),
4025
4026 play: html5_event(function() {
4027
4028 // sm2._wD(this._s.id + ': play()');
4029 // once play starts, no buffering
4030 this._s._onbufferchange(0);
4031
4032 }),
4033
4034 playing: html5_event(function() {
4035
4036 sm2._wD(this._s.id + ': playing ' + String.fromCharCode(9835));
4037 // once play starts, no buffering
4038 this._s._onbufferchange(0);
4039
4040 }),
4041
4042 progress: html5_event(function(e) {
4043
4044 // note: can fire repeatedly after "loaded" event, due to use of HTTP range/partials
4045
4046 var s = this._s,
4047 i, j, progStr, buffered = 0,
4048 isProgress = (e.type === 'progress'),
4049 ranges = e.target.buffered,
4050 // firefox 3.6 implements e.loaded/total (bytes)
4051 loaded = (e.loaded || 0),
4052 total = (e.total || 1);
4053
4054 // reset the "buffered" (loaded byte ranges) array
4055 s.buffered = [];
4056
4057 if (ranges && ranges.length) {
4058
4059 // if loaded is 0, try TimeRanges implementation as % of load
4060 // https://developer.mozilla.org/en/DOM/TimeRanges
4061
4062 // re-build "buffered" array
4063 // HTML5 returns seconds. SM2 API uses msec for setPosition() etc., whether Flash or HTML5.
4064 for (i = 0, j = ranges.length; i < j; i++) {
4065 s.buffered.push({
4066 start: ranges.start(i) * msecScale,
4067 end: ranges.end(i) * msecScale
4068 });
4069 }
4070
4071 // use the last value locally
4072 buffered = (ranges.end(0) - ranges.start(0)) * msecScale;
4073
4074 // linear case, buffer sum; does not account for seeking and HTTP partials / byte ranges
4075 loaded = Math.min(1, buffered / (e.target.duration * msecScale));
4076
4077 // <d>
4078 if (isProgress && ranges.length > 1) {
4079 progStr = [];
4080 j = ranges.length;
4081 for (i = 0; i < j; i++) {
4082 progStr.push((e.target.buffered.start(i) * msecScale) + '-' + (e.target.buffered.end(i) * msecScale));
4083 }
4084 sm2._wD(this._s.id + ': progress, timeRanges: ' + progStr.join(', '));
4085 }
4086
4087 if (isProgress && !isNaN(loaded)) {
4088 sm2._wD(this._s.id + ': progress, ' + Math.floor(loaded * 100) + '% loaded');
4089 }
4090 // </d>
4091
4092 }
4093
4094 if (!isNaN(loaded)) {
4095
4096 // TODO: prevent calls with duplicate values.
4097 s._whileloading(loaded, total, s._get_html5_duration());
4098 if (loaded && total && loaded === total) {
4099 // in case "onload" doesn't fire (eg. gecko 1.9.2)
4100 html5_events.canplaythrough.call(this, e);
4101 }
4102
4103 }
4104
4105 }),
4106
4107 ratechange: html5_event(function() {
4108
4109 sm2._wD(this._s.id + ': ratechange');
4110
4111 }),
4112
4113 suspend: html5_event(function(e) {
4114
4115 // download paused/stopped, may have finished (eg. onload)
4116 var s = this._s;
4117
4118 sm2._wD(this._s.id + ': suspend');
4119 html5_events.progress.call(this, e);
4120 s._onsuspend();
4121
4122 }),
4123
4124 stalled: html5_event(function() {
4125
4126 sm2._wD(this._s.id + ': stalled');
4127
4128 }),
4129
4130 timeupdate: html5_event(function() {
4131
4132 this._s._onTimer();
4133
4134 }),
4135
4136 waiting: html5_event(function() {
4137
4138 var s = this._s;
4139
4140 // see also: seeking
4141 sm2._wD(this._s.id + ': waiting');
4142
4143 // playback faster than download rate, etc.
4144 s._onbufferchange(1);
4145
4146 })
4147
4148 };
4149
4150 html5OK = function(iO) {
4151
4152 // playability test based on URL or MIME type
4153
4154 var result;
4155
4156 if (!iO || (!iO.type && !iO.url && !iO.serverURL)) {
4157
4158 // nothing to check
4159 result = false;
4160
4161 } else if (iO.serverURL || (iO.type && preferFlashCheck(iO.type))) {
4162
4163 // RTMP, or preferring flash
4164 result = false;
4165
4166 } else {
4167
4168 // Use type, if specified. Pass data: URIs to HTML5. If HTML5-only mode, no other options, so just give 'er
4169 result = ((iO.type ? html5CanPlay({ type: iO.type }) : html5CanPlay({ url: iO.url }) || sm2.html5Only || iO.url.match(/data:/i)));
4170
4171 }
4172
4173 return result;
4174
4175 };
4176
4177 html5Unload = function(oAudio) {
4178
4179 /**
4180 * Internal method: Unload media, and cancel any current/pending network requests.
4181 * Firefox can load an empty URL, which allegedly destroys the decoder and stops the download.
4182 * https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Stopping_the_download_of_media
4183 * However, Firefox has been seen loading a relative URL from '' and thus requesting the hosting page on unload.
4184 * Other UA behaviour is unclear, so everyone else gets an about:blank-style URL.
4185 */
4186
4187 var url;
4188
4189 if (oAudio) {
4190
4191 // Firefox and Chrome accept short WAVe data: URIs. Chome dislikes audio/wav, but accepts audio/wav for data: MIME.
4192 // Desktop Safari complains / fails on data: URI, so it gets about:blank.
4193 url = (isSafari ? emptyURL : (sm2.html5.canPlayType('audio/wav') ? emptyWAV : emptyURL));
4194
4195 oAudio.src = url;
4196
4197 // reset some state, too
4198 if (oAudio._called_unload !== _undefined) {
4199 oAudio._called_load = false;
4200 }
4201
4202 }
4203
4204 if (useGlobalHTML5Audio) {
4205
4206 // ensure URL state is trashed, also
4207 lastGlobalHTML5URL = null;
4208
4209 }
4210
4211 return url;
4212
4213 };
4214
4215 html5CanPlay = function(o) {
4216
4217 /**
4218 * Try to find MIME, test and return truthiness
4219 * o = {
4220 * url: '/path/to/an.mp3',
4221 * type: 'audio/mp3'
4222 * }
4223 */
4224
4225 if (!sm2.useHTML5Audio || !sm2.hasHTML5) return false;
4226
4227 var url = (o.url || null),
4228 mime = (o.type || null),
4229 aF = sm2.audioFormats,
4230 result,
4231 offset,
4232 fileExt,
4233 item;
4234
4235 // account for known cases like audio/mp3
4236
4237 if (mime && sm2.html5[mime] !== _undefined) return (sm2.html5[mime] && !preferFlashCheck(mime));
4238
4239 if (!html5Ext) {
4240
4241 html5Ext = [];
4242
4243 for (item in aF) {
4244
4245 if (aF.hasOwnProperty(item)) {
4246
4247 html5Ext.push(item);
4248
4249 if (aF[item].related) {
4250 html5Ext = html5Ext.concat(aF[item].related);
4251 }
4252
4253 }
4254
4255 }
4256
4257 html5Ext = new RegExp('\\.(' + html5Ext.join('|') + ')(\\?.*)?$', 'i');
4258
4259 }
4260
4261 // TODO: Strip URL queries, etc.
4262 fileExt = (url ? url.toLowerCase().match(html5Ext) : null);
4263
4264 if (!fileExt || !fileExt.length) {
4265
4266 if (!mime) {
4267
4268 result = false;
4269
4270 } else {
4271
4272 // audio/mp3 -> mp3, result should be known
4273 offset = mime.indexOf(';');
4274
4275 // strip "audio/X; codecs..."
4276 fileExt = (offset !== -1 ? mime.substr(0, offset) : mime).substr(6);
4277
4278 }
4279
4280 } else {
4281
4282 // match the raw extension name - "mp3", for example
4283 fileExt = fileExt[1];
4284
4285 }
4286
4287 if (fileExt && sm2.html5[fileExt] !== _undefined) {
4288
4289 // result known
4290 result = (sm2.html5[fileExt] && !preferFlashCheck(fileExt));
4291
4292 } else {
4293
4294 mime = 'audio/' + fileExt;
4295 result = sm2.html5.canPlayType({ type: mime });
4296
4297 sm2.html5[fileExt] = result;
4298
4299 // sm2._wD('canPlayType, found result: ' + result);
4300 result = (result && sm2.html5[mime] && !preferFlashCheck(mime));
4301 }
4302
4303 return result;
4304
4305 };
4306
4307 testHTML5 = function() {
4308
4309 /**
4310 * Internal: Iterates over audioFormats, determining support eg. audio/mp3, audio/mpeg and so on
4311 * assigns results to html5[] and flash[].
4312 */
4313
4314 if (!sm2.useHTML5Audio || !sm2.hasHTML5) {
4315
4316 // without HTML5, we need Flash.
4317 sm2.html5.usingFlash = true;
4318 needsFlash = true;
4319
4320 return false;
4321
4322 }
4323
4324 // 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. :/
4325 var a = (Audio !== _undefined ? (isOpera && opera.version() < 10 ? new Audio(null) : new Audio()) : null),
4326 item, lookup, support = {}, aF, i;
4327
4328 function cp(m) {
4329
4330 var canPlay, j,
4331 result = false,
4332 isOK = false;
4333
4334 if (!a || typeof a.canPlayType !== 'function') return result;
4335
4336 if (m instanceof Array) {
4337
4338 // iterate through all mime types, return any successes
4339
4340 for (i = 0, j = m.length; i < j; i++) {
4341
4342 if (sm2.html5[m[i]] || a.canPlayType(m[i]).match(sm2.html5Test)) {
4343
4344 isOK = true;
4345 sm2.html5[m[i]] = true;
4346
4347 // note flash support, too
4348 sm2.flash[m[i]] = !!(m[i].match(flashMIME));
4349
4350 }
4351
4352 }
4353
4354 result = isOK;
4355
4356 } else {
4357
4358 canPlay = (a && typeof a.canPlayType === 'function' ? a.canPlayType(m) : false);
4359 result = !!(canPlay && (canPlay.match(sm2.html5Test)));
4360
4361 }
4362
4363 return result;
4364
4365 }
4366
4367 // test all registered formats + codecs
4368
4369 aF = sm2.audioFormats;
4370
4371 for (item in aF) {
4372
4373 if (aF.hasOwnProperty(item)) {
4374
4375 lookup = 'audio/' + item;
4376
4377 support[item] = cp(aF[item].type);
4378
4379 // write back generic type too, eg. audio/mp3
4380 support[lookup] = support[item];
4381
4382 // assign flash
4383 if (item.match(flashMIME)) {
4384
4385 sm2.flash[item] = true;
4386 sm2.flash[lookup] = true;
4387
4388 } else {
4389
4390 sm2.flash[item] = false;
4391 sm2.flash[lookup] = false;
4392
4393 }
4394
4395 // assign result to related formats, too
4396
4397 if (aF[item] && aF[item].related) {
4398
4399 for (i = aF[item].related.length - 1; i >= 0; i--) {
4400
4401 // eg. audio/m4a
4402 support['audio/' + aF[item].related[i]] = support[item];
4403 sm2.html5[aF[item].related[i]] = support[item];
4404 sm2.flash[aF[item].related[i]] = support[item];
4405
4406 }
4407
4408 }
4409
4410 }
4411
4412 }
4413
4414 support.canPlayType = (a ? cp : null);
4415 sm2.html5 = mixin(sm2.html5, support);
4416
4417 sm2.html5.usingFlash = featureCheck();
4418 needsFlash = sm2.html5.usingFlash;
4419
4420 return true;
4421
4422 };
4423
4424 strings = {
4425
4426 // <d>
4427 notReady: 'Unavailable - wait until onready() has fired.',
4428 notOK: 'Audio support is not available.',
4429 domError: sm + 'exception caught while appending SWF to DOM.',
4430 spcWmode: 'Removing wmode, preventing known SWF loading issue(s)',
4431 swf404: smc + 'Verify that %s is a valid path.',
4432 tryDebug: 'Try ' + sm + '.debugFlash = true for more security details (output goes to SWF.)',
4433 checkSWF: 'See SWF output for more debug info.',
4434 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/',
4435 waitFocus: smc + 'Special case: Waiting for SWF to load with window focus...',
4436 waitForever: smc + 'Waiting indefinitely for Flash (will recover if unblocked)...',
4437 waitSWF: smc + 'Waiting for 100% SWF load...',
4438 needFunction: smc + 'Function object expected for %s',
4439 badID: 'Sound ID "%s" should be a string, starting with a non-numeric character',
4440 currentObj: smc + '_debug(): Current sound objects',
4441 waitOnload: smc + 'Waiting for window.onload()',
4442 docLoaded: smc + 'Document already loaded',
4443 onload: smc + 'initComplete(): calling soundManager.onload()',
4444 onloadOK: sm + '.onload() complete',
4445 didInit: smc + 'init(): Already called?',
4446 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',
4447 badRemove: smc + 'Failed to remove Flash node.',
4448 shutdown: sm + '.disable(): Shutting down',
4449 queue: smc + 'Queueing %s handler',
4450 smError: 'SMSound.load(): Exception: JS-Flash communication failed, or JS error.',
4451 fbTimeout: 'No flash response, applying .' + swfCSS.swfTimedout + ' CSS...',
4452 fbLoaded: 'Flash loaded',
4453 fbHandler: smc + 'flashBlockHandler()',
4454 manURL: 'SMSound.load(): Using manually-assigned URL',
4455 onURL: sm + '.load(): current URL already assigned.',
4456 badFV: sm + '.flashVersion must be 8 or 9. "%s" is invalid. Reverting to %s.',
4457 as2loop: 'Note: Setting stream:false so looping can work (flash 8 limitation)',
4458 noNSLoop: 'Note: Looping not implemented for MovieStar formats',
4459 needfl9: 'Note: Switching to flash 9, required for MP4 formats.',
4460 mfTimeout: 'Setting flashLoadTimeout = 0 (infinite) for off-screen, mobile flash case',
4461 needFlash: smc + 'Fatal error: Flash is needed to play some required formats, but is not available.',
4462 gotFocus: smc + 'Got window focus.',
4463 policy: 'Enabling usePolicyFile for data access',
4464 setup: sm + '.setup(): allowed parameters: %s',
4465 setupError: sm + '.setup(): "%s" cannot be assigned with this method.',
4466 setupUndef: sm + '.setup(): Could not find option "%s"',
4467 setupLate: sm + '.setup(): url, flashVersion and html5Test property changes will not take effect until reboot().',
4468 noURL: smc + 'Flash URL required. Call soundManager.setup({url:...}) to get started.',
4469 sm2Loaded: 'SoundManager 2: Ready. ' + String.fromCharCode(10003),
4470 reset: sm + '.reset(): Removing event callbacks',
4471 mobileUA: 'Mobile UA detected, preferring HTML5 by default.',
4472 globalHTML5: 'Using singleton HTML5 Audio() pattern for this device.',
4473 ignoreMobile: 'Ignoring mobile restrictions for this device.'
4474 // </d>
4475
4476 };
4477
4478 str = function() {
4479
4480 // internal string replace helper.
4481 // arguments: o [,items to replace]
4482 // <d>
4483
4484 var args,
4485 i, j, o,
4486 sstr;
4487
4488 // real array, please
4489 args = slice.call(arguments);
4490
4491 // first argument
4492 o = args.shift();
4493
4494 sstr = (strings && strings[o] ? strings[o] : '');
4495
4496 if (sstr && args && args.length) {
4497 for (i = 0, j = args.length; i < j; i++) {
4498 sstr = sstr.replace('%s', args[i]);
4499 }
4500 }
4501
4502 return sstr;
4503 // </d>
4504
4505 };
4506
4507 loopFix = function(sOpt) {
4508
4509 // flash 8 requires stream = false for looping to work
4510 if (fV === 8 && sOpt.loops > 1 && sOpt.stream) {
4511 _wDS('as2loop');
4512 sOpt.stream = false;
4513 }
4514
4515 return sOpt;
4516
4517 };
4518
4519 policyFix = function(sOpt, sPre) {
4520
4521 if (sOpt && !sOpt.usePolicyFile && (sOpt.onid3 || sOpt.usePeakData || sOpt.useWaveformData || sOpt.useEQData)) {
4522 sm2._wD((sPre || '') + str('policy'));
4523 sOpt.usePolicyFile = true;
4524 }
4525
4526 return sOpt;
4527
4528 };
4529
4530 complain = function(sMsg) {
4531
4532 // <d>
4533 if (hasConsole && console.warn !== _undefined) {
4534 console.warn(sMsg);
4535 } else {
4536 sm2._wD(sMsg);
4537 }
4538 // </d>
4539
4540 };
4541
4542 doNothing = function() {
4543
4544 return false;
4545
4546 };
4547
4548 disableObject = function(o) {
4549
4550 var oProp;
4551
4552 for (oProp in o) {
4553 if (o.hasOwnProperty(oProp) && typeof o[oProp] === 'function') {
4554 o[oProp] = doNothing;
4555 }
4556 }
4557
4558 oProp = null;
4559
4560 };
4561
4562 failSafely = function(bNoDisable) {
4563
4564 // general failure exception handler
4565
4566 if (bNoDisable === _undefined) {
4567 bNoDisable = false;
4568 }
4569
4570 if (disabled || bNoDisable) {
4571 sm2.disable(bNoDisable);
4572 }
4573
4574 };
4575
4576 normalizeMovieURL = function(movieURL) {
4577
4578 var urlParams = null, url;
4579
4580 if (movieURL) {
4581
4582 if (movieURL.match(/\.swf(\?.*)?$/i)) {
4583
4584 urlParams = movieURL.substr(movieURL.toLowerCase().lastIndexOf('.swf?') + 4);
4585
4586 // assume user knows what they're doing
4587 if (urlParams) return movieURL;
4588
4589 } else if (movieURL.lastIndexOf('/') !== movieURL.length - 1) {
4590
4591 // append trailing slash, if needed
4592 movieURL += '/';
4593
4594 }
4595
4596 }
4597
4598 url = (movieURL && movieURL.lastIndexOf('/') !== -1 ? movieURL.substr(0, movieURL.lastIndexOf('/') + 1) : './') + sm2.movieURL;
4599
4600 if (sm2.noSWFCache) {
4601 url += ('?ts=' + new Date().getTime());
4602 }
4603
4604 return url;
4605
4606 };
4607
4608 setVersionInfo = function() {
4609
4610 // short-hand for internal use
4611
4612 fV = parseInt(sm2.flashVersion, 10);
4613
4614 if (fV !== 8 && fV !== 9) {
4615 sm2._wD(str('badFV', fV, defaultFlashVersion));
4616 sm2.flashVersion = fV = defaultFlashVersion;
4617 }
4618
4619 // debug flash movie, if applicable
4620
4621 var isDebug = (sm2.debugMode || sm2.debugFlash ? '_debug.swf' : '.swf');
4622
4623 if (sm2.useHTML5Audio && !sm2.html5Only && sm2.audioFormats.mp4.required && fV < 9) {
4624 sm2._wD(str('needfl9'));
4625 sm2.flashVersion = fV = 9;
4626 }
4627
4628 sm2.version = sm2.versionNumber + (sm2.html5Only ? ' (HTML5-only mode)' : (fV === 9 ? ' (AS3/Flash 9)' : ' (AS2/Flash 8)'));
4629
4630 // set up default options
4631 if (fV > 8) {
4632
4633 // +flash 9 base options
4634 sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.flash9Options);
4635 sm2.features.buffering = true;
4636
4637 // +moviestar support
4638 sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.movieStarOptions);
4639 sm2.filePatterns.flash9 = new RegExp('\\.(mp3|' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i');
4640 sm2.features.movieStar = true;
4641
4642 } else {
4643
4644 sm2.features.movieStar = false;
4645
4646 }
4647
4648 // regExp for flash canPlay(), etc.
4649 sm2.filePattern = sm2.filePatterns[(fV !== 8 ? 'flash9' : 'flash8')];
4650
4651 // if applicable, use _debug versions of SWFs
4652 sm2.movieURL = (fV === 8 ? 'soundmanager2.swf' : 'soundmanager2_flash9.swf').replace('.swf', isDebug);
4653
4654 sm2.features.peakData = sm2.features.waveformData = sm2.features.eqData = (fV > 8);
4655
4656 };
4657
4658 setPolling = function(bPolling, bHighPerformance) {
4659
4660 if (!flash) {
4661 return;
4662 }
4663
4664 flash._setPolling(bPolling, bHighPerformance);
4665
4666 };
4667
4668 initDebug = function() {
4669
4670 // starts debug mode, creating output <div> for UAs without console object
4671
4672 // allow force of debug mode via URL
4673 // <d>
4674 if (sm2.debugURLParam.test(wl)) {
4675 sm2.setupOptions.debugMode = sm2.debugMode = true;
4676 }
4677
4678 if (id(sm2.debugID)) {
4679 return;
4680 }
4681
4682 var oD, oDebug, oTarget, oToggle, tmp;
4683
4684 if (sm2.debugMode && !id(sm2.debugID) && (!hasConsole || !sm2.useConsole || !sm2.consoleOnly)) {
4685
4686 oD = doc.createElement('div');
4687 oD.id = sm2.debugID + '-toggle';
4688
4689 oToggle = {
4690 position: 'fixed',
4691 bottom: '0px',
4692 right: '0px',
4693 width: '1.2em',
4694 height: '1.2em',
4695 lineHeight: '1.2em',
4696 margin: '2px',
4697 textAlign: 'center',
4698 border: '1px solid #999',
4699 cursor: 'pointer',
4700 background: '#fff',
4701 color: '#333',
4702 zIndex: 10001
4703 };
4704
4705 oD.appendChild(doc.createTextNode('-'));
4706 oD.onclick = toggleDebug;
4707 oD.title = 'Toggle SM2 debug console';
4708
4709 if (ua.match(/msie 6/i)) {
4710 oD.style.position = 'absolute';
4711 oD.style.cursor = 'hand';
4712 }
4713
4714 for (tmp in oToggle) {
4715 if (oToggle.hasOwnProperty(tmp)) {
4716 oD.style[tmp] = oToggle[tmp];
4717 }
4718 }
4719
4720 oDebug = doc.createElement('div');
4721 oDebug.id = sm2.debugID;
4722 oDebug.style.display = (sm2.debugMode ? 'block' : 'none');
4723
4724 if (sm2.debugMode && !id(oD.id)) {
4725 try {
4726 oTarget = getDocument();
4727 oTarget.appendChild(oD);
4728 } catch(e2) {
4729 throw new Error(str('domError') + ' \n' + e2.toString());
4730 }
4731 oTarget.appendChild(oDebug);
4732 }
4733
4734 }
4735
4736 oTarget = null;
4737 // </d>
4738
4739 };
4740
4741 idCheck = this.getSoundById;
4742
4743 // <d>
4744 _wDS = function(o, errorLevel) {
4745
4746 return (!o ? '' : sm2._wD(str(o), errorLevel));
4747
4748 };
4749
4750 toggleDebug = function() {
4751
4752 var o = id(sm2.debugID),
4753 oT = id(sm2.debugID + '-toggle');
4754
4755 if (!o) {
4756 return;
4757 }
4758
4759 if (debugOpen) {
4760 // minimize
4761 oT.innerHTML = '+';
4762 o.style.display = 'none';
4763 } else {
4764 oT.innerHTML = '-';
4765 o.style.display = 'block';
4766 }
4767
4768 debugOpen = !debugOpen;
4769
4770 };
4771
4772 debugTS = function(sEventType, bSuccess, sMessage) {
4773
4774 // troubleshooter debug hooks
4775
4776 if (window.sm2Debugger !== _undefined) {
4777 try {
4778 sm2Debugger.handleEvent(sEventType, bSuccess, sMessage);
4779 } catch(e) {
4780 // oh well
4781 return false;
4782 }
4783 }
4784
4785 return true;
4786
4787 };
4788 // </d>
4789
4790 getSWFCSS = function() {
4791
4792 var css = [];
4793
4794 if (sm2.debugMode) {
4795 css.push(swfCSS.sm2Debug);
4796 }
4797
4798 if (sm2.debugFlash) {
4799 css.push(swfCSS.flashDebug);
4800 }
4801
4802 if (sm2.useHighPerformance) {
4803 css.push(swfCSS.highPerf);
4804 }
4805
4806 return css.join(' ');
4807
4808 };
4809
4810 flashBlockHandler = function() {
4811
4812 // *possible* flash block situation.
4813
4814 var name = str('fbHandler'),
4815 p = sm2.getMoviePercent(),
4816 css = swfCSS,
4817 error = {
4818 type: 'FLASHBLOCK'
4819 };
4820
4821 if (sm2.html5Only) {
4822 // no flash, or unused
4823 return;
4824 }
4825
4826 if (!sm2.ok()) {
4827
4828 if (needsFlash) {
4829 // make the movie more visible, so user can fix
4830 sm2.oMC.className = getSWFCSS() + ' ' + css.swfDefault + ' ' + (p === null ? css.swfTimedout : css.swfError);
4831 sm2._wD(name + ': ' + str('fbTimeout') + (p ? ' (' + str('fbLoaded') + ')' : ''));
4832 }
4833
4834 sm2.didFlashBlock = true;
4835
4836 // fire onready(), complain lightly
4837 processOnEvents({
4838 type: 'ontimeout',
4839 ignoreInit: true,
4840 error: error
4841 });
4842
4843 catchError(error);
4844
4845 } else {
4846
4847 // SM2 loaded OK (or recovered)
4848
4849 // <d>
4850 if (sm2.didFlashBlock) {
4851 sm2._wD(name + ': Unblocked');
4852 }
4853 // </d>
4854
4855 if (sm2.oMC) {
4856 sm2.oMC.className = [getSWFCSS(), css.swfDefault, css.swfLoaded + (sm2.didFlashBlock ? ' ' + css.swfUnblocked : '')].join(' ');
4857 }
4858
4859 }
4860
4861 };
4862
4863 addOnEvent = function(sType, oMethod, oScope) {
4864
4865 if (on_queue[sType] === _undefined) {
4866 on_queue[sType] = [];
4867 }
4868
4869 on_queue[sType].push({
4870 method: oMethod,
4871 scope: (oScope || null),
4872 fired: false
4873 });
4874
4875 };
4876
4877 processOnEvents = function(oOptions) {
4878
4879 // if unspecified, assume OK/error
4880
4881 if (!oOptions) {
4882 oOptions = {
4883 type: (sm2.ok() ? 'onready' : 'ontimeout')
4884 };
4885 }
4886
4887 // not ready yet.
4888 if (!didInit && oOptions && !oOptions.ignoreInit) return false;
4889
4890 // invalid case
4891 if (oOptions.type === 'ontimeout' && (sm2.ok() || (disabled && !oOptions.ignoreInit))) return false;
4892
4893 var status = {
4894 success: (oOptions && oOptions.ignoreInit ? sm2.ok() : !disabled)
4895 },
4896
4897 // queue specified by type, or none
4898 srcQueue = (oOptions && oOptions.type ? on_queue[oOptions.type] || [] : []),
4899
4900 queue = [], i, j,
4901 args = [status],
4902 canRetry = (needsFlash && !sm2.ok());
4903
4904 if (oOptions.error) {
4905 args[0].error = oOptions.error;
4906 }
4907
4908 for (i = 0, j = srcQueue.length; i < j; i++) {
4909 if (srcQueue[i].fired !== true) {
4910 queue.push(srcQueue[i]);
4911 }
4912 }
4913
4914 if (queue.length) {
4915
4916 // sm2._wD(sm + ': Firing ' + queue.length + ' ' + oOptions.type + '() item' + (queue.length === 1 ? '' : 's'));
4917 for (i = 0, j = queue.length; i < j; i++) {
4918
4919 if (queue[i].scope) {
4920 queue[i].method.apply(queue[i].scope, args);
4921 } else {
4922 queue[i].method.apply(this, args);
4923 }
4924
4925 if (!canRetry) {
4926 // useFlashBlock and SWF timeout case doesn't count here.
4927 queue[i].fired = true;
4928
4929 }
4930
4931 }
4932
4933 }
4934
4935 return true;
4936
4937 };
4938
4939 initUserOnload = function() {
4940
4941 window.setTimeout(function() {
4942
4943 if (sm2.useFlashBlock) {
4944 flashBlockHandler();
4945 }
4946
4947 processOnEvents();
4948
4949 // call user-defined "onload", scoped to window
4950
4951 if (typeof sm2.onload === 'function') {
4952 _wDS('onload', 1);
4953 sm2.onload.apply(window);
4954 _wDS('onloadOK', 1);
4955 }
4956
4957 if (sm2.waitForWindowLoad) {
4958 event.add(window, 'load', initUserOnload);
4959 }
4960
4961 }, 1);
4962
4963 };
4964
4965 detectFlash = function() {
4966
4967 /**
4968 * Hat tip: Flash Detect library (BSD, (C) 2007) by Carl "DocYes" S. Yestrau
4969 * http://featureblend.com/javascript-flash-detection-library.html / http://featureblend.com/license.txt
4970 */
4971
4972 // this work has already been done.
4973 if (hasFlash !== _undefined) return hasFlash;
4974
4975 var hasPlugin = false, n = navigator, obj, type, types, AX = window.ActiveXObject;
4976
4977 // MS Edge 14 throws an "Unspecified Error" because n.plugins is inaccessible due to permissions
4978 var nP;
4979
4980 try {
4981 nP = n.plugins;
4982 } catch(e) {
4983 nP = undefined;
4984 }
4985
4986 if (nP && nP.length) {
4987
4988 type = 'application/x-shockwave-flash';
4989 types = n.mimeTypes;
4990
4991 if (types && types[type] && types[type].enabledPlugin && types[type].enabledPlugin.description) {
4992 hasPlugin = true;
4993 }
4994
4995 } else if (AX !== _undefined && !ua.match(/MSAppHost/i)) {
4996
4997 // 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.
4998 try {
4999 obj = new AX('ShockwaveFlash.ShockwaveFlash');
5000 } catch(e) {
5001 // oh well
5002 obj = null;
5003 }
5004
5005 hasPlugin = (!!obj);
5006
5007 // cleanup, because it is ActiveX after all
5008 obj = null;
5009
5010 }
5011
5012 hasFlash = hasPlugin;
5013
5014 return hasPlugin;
5015
5016 };
5017
5018 featureCheck = function() {
5019
5020 var flashNeeded,
5021 item,
5022 formats = sm2.audioFormats,
5023 // iPhone <= 3.1 has broken HTML5 audio(), but firmware 3.2 (original iPad) + iOS4 works.
5024 isSpecial = (is_iDevice && !!(ua.match(/os (1|2|3_0|3_1)\s/i)));
5025
5026 if (isSpecial) {
5027
5028 // has Audio(), but is broken; let it load links directly.
5029 sm2.hasHTML5 = false;
5030
5031 // ignore flash case, however
5032 sm2.html5Only = true;
5033
5034 // hide the SWF, if present
5035 if (sm2.oMC) {
5036 sm2.oMC.style.display = 'none';
5037 }
5038
5039 } else if (sm2.useHTML5Audio) {
5040
5041 if (!sm2.html5 || !sm2.html5.canPlayType) {
5042 sm2._wD('SoundManager: No HTML5 Audio() support detected.');
5043 sm2.hasHTML5 = false;
5044 }
5045
5046 // <d>
5047 if (isBadSafari) {
5048 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);
5049 }
5050 // </d>
5051
5052 }
5053
5054 if (sm2.useHTML5Audio && sm2.hasHTML5) {
5055
5056 // sort out whether flash is optional, required or can be ignored.
5057
5058 // innocent until proven guilty.
5059 canIgnoreFlash = true;
5060
5061 for (item in formats) {
5062
5063 if (formats.hasOwnProperty(item)) {
5064
5065 if (formats[item].required) {
5066
5067 if (!sm2.html5.canPlayType(formats[item].type)) {
5068
5069 // 100% HTML5 mode is not possible.
5070 canIgnoreFlash = false;
5071 flashNeeded = true;
5072
5073 } else if (sm2.preferFlash && (sm2.flash[item] || sm2.flash[formats[item].type])) {
5074
5075 // flash may be required, or preferred for this format.
5076 flashNeeded = true;
5077
5078 }
5079
5080 }
5081
5082 }
5083
5084 }
5085
5086 }
5087
5088 // sanity check...
5089 if (sm2.ignoreFlash) {
5090 flashNeeded = false;
5091 canIgnoreFlash = true;
5092 }
5093
5094 sm2.html5Only = (sm2.hasHTML5 && sm2.useHTML5Audio && !flashNeeded);
5095
5096 return (!sm2.html5Only);
5097
5098 };
5099
5100 parseURL = function(url) {
5101
5102 /**
5103 * Internal: Finds and returns the first playable URL (or failing that, the first URL.)
5104 * @param {string or array} url A single URL string, OR, an array of URL strings or {url:'/path/to/resource', type:'audio/mp3'} objects.
5105 */
5106
5107 var i, j, urlResult = 0, result;
5108
5109 if (url instanceof Array) {
5110
5111 // find the first good one
5112 for (i = 0, j = url.length; i < j; i++) {
5113
5114 if (url[i] instanceof Object) {
5115
5116 // MIME check
5117 if (sm2.canPlayMIME(url[i].type)) {
5118 urlResult = i;
5119 break;
5120 }
5121
5122 } else if (sm2.canPlayURL(url[i])) {
5123
5124 // URL string check
5125 urlResult = i;
5126 break;
5127
5128 }
5129
5130 }
5131
5132 // normalize to string
5133 if (url[urlResult].url) {
5134 url[urlResult] = url[urlResult].url;
5135 }
5136
5137 result = url[urlResult];
5138
5139 } else {
5140
5141 // single URL case
5142 result = url;
5143
5144 }
5145
5146 return result;
5147
5148 };
5149
5150
5151 startTimer = function(oSound) {
5152
5153 /**
5154 * attach a timer to this sound, and start an interval if needed
5155 */
5156
5157 if (!oSound._hasTimer) {
5158
5159 oSound._hasTimer = true;
5160
5161 if (!mobileHTML5 && sm2.html5PollingInterval) {
5162
5163 if (h5IntervalTimer === null && h5TimerCount === 0) {
5164
5165 h5IntervalTimer = setInterval(timerExecute, sm2.html5PollingInterval);
5166
5167 }
5168
5169 h5TimerCount++;
5170
5171 }
5172
5173 }
5174
5175 };
5176
5177 stopTimer = function(oSound) {
5178
5179 /**
5180 * detach a timer
5181 */
5182
5183 if (oSound._hasTimer) {
5184
5185 oSound._hasTimer = false;
5186
5187 if (!mobileHTML5 && sm2.html5PollingInterval) {
5188
5189 // interval will stop itself at next execution.
5190
5191 h5TimerCount--;
5192
5193 }
5194
5195 }
5196
5197 };
5198
5199 timerExecute = function() {
5200
5201 /**
5202 * manual polling for HTML5 progress events, ie., whileplaying()
5203 * (can achieve greater precision than conservative default HTML5 interval)
5204 */
5205
5206 var i;
5207
5208 if (h5IntervalTimer !== null && !h5TimerCount) {
5209
5210 // no active timers, stop polling interval.
5211
5212 clearInterval(h5IntervalTimer);
5213
5214 h5IntervalTimer = null;
5215
5216 return;
5217
5218 }
5219
5220 // check all HTML5 sounds with timers
5221
5222 for (i = sm2.soundIDs.length - 1; i >= 0; i--) {
5223
5224 if (sm2.sounds[sm2.soundIDs[i]].isHTML5 && sm2.sounds[sm2.soundIDs[i]]._hasTimer) {
5225 sm2.sounds[sm2.soundIDs[i]]._onTimer();
5226 }
5227
5228 }
5229
5230 };
5231
5232 catchError = function(options) {
5233
5234 options = (options !== _undefined ? options : {});
5235
5236 if (typeof sm2.onerror === 'function') {
5237 sm2.onerror.apply(window, [{
5238 type: (options.type !== _undefined ? options.type : null)
5239 }]);
5240 }
5241
5242 if (options.fatal !== _undefined && options.fatal) {
5243 sm2.disable();
5244 }
5245
5246 };
5247
5248 badSafariFix = function() {
5249
5250 // special case: "bad" Safari (OS X 10.3 - 10.7) must fall back to flash for MP3/MP4
5251 if (!isBadSafari || !detectFlash()) {
5252 // doesn't apply
5253 return;
5254 }
5255
5256 var aF = sm2.audioFormats, i, item;
5257
5258 for (item in aF) {
5259
5260 if (aF.hasOwnProperty(item)) {
5261
5262 if (item === 'mp3' || item === 'mp4') {
5263
5264 sm2._wD(sm + ': Using flash fallback for ' + item + ' format');
5265 sm2.html5[item] = false;
5266
5267 // assign result to related formats, too
5268 if (aF[item] && aF[item].related) {
5269 for (i = aF[item].related.length - 1; i >= 0; i--) {
5270 sm2.html5[aF[item].related[i]] = false;
5271 }
5272 }
5273
5274 }
5275
5276 }
5277
5278 }
5279
5280 };
5281
5282 /**
5283 * Pseudo-private flash/ExternalInterface methods
5284 * ----------------------------------------------
5285 */
5286
5287 this._setSandboxType = function(sandboxType) {
5288
5289 // <d>
5290 // Security sandbox according to Flash plugin
5291 var sb = sm2.sandbox;
5292
5293 sb.type = sandboxType;
5294 sb.description = sb.types[(sb.types[sandboxType] !== _undefined ? sandboxType : 'unknown')];
5295
5296 if (sb.type === 'localWithFile') {
5297
5298 sb.noRemote = true;
5299 sb.noLocal = false;
5300 _wDS('secNote', 2);
5301
5302 } else if (sb.type === 'localWithNetwork') {
5303
5304 sb.noRemote = false;
5305 sb.noLocal = true;
5306
5307 } else if (sb.type === 'localTrusted') {
5308
5309 sb.noRemote = false;
5310 sb.noLocal = false;
5311
5312 }
5313 // </d>
5314
5315 };
5316
5317 this._externalInterfaceOK = function(swfVersion) {
5318
5319 // flash callback confirming flash loaded, EI working etc.
5320 // swfVersion: SWF build string
5321
5322 if (sm2.swfLoaded) {
5323 return;
5324 }
5325
5326 var e;
5327
5328 debugTS('swf', true);
5329 debugTS('flashtojs', true);
5330 sm2.swfLoaded = true;
5331 tryInitOnFocus = false;
5332
5333 if (isBadSafari) {
5334 badSafariFix();
5335 }
5336
5337 // complain if JS + SWF build/version strings don't match, excluding +DEV builds
5338 // <d>
5339 if (!swfVersion || swfVersion.replace(/\+dev/i, '') !== sm2.versionNumber.replace(/\+dev/i, '')) {
5340
5341 e = sm + ': Fatal: JavaScript file build "' + sm2.versionNumber + '" does not match Flash SWF build "' + swfVersion + '" at ' + sm2.url + '. Ensure both are up-to-date.';
5342
5343 // escape flash -> JS stack so this error fires in window.
5344 setTimeout(function() {
5345 throw new Error(e);
5346 }, 0);
5347
5348 // exit, init will fail with timeout
5349 return;
5350
5351 }
5352 // </d>
5353
5354 // IE needs a larger timeout
5355 setTimeout(init, isIE ? 100 : 1);
5356
5357 };
5358
5359 /**
5360 * Private initialization helpers
5361 * ------------------------------
5362 */
5363
5364 createMovie = function(movieID, movieURL) {
5365
5366 // ignore if already connected
5367 if (didAppend && appendSuccess) return false;
5368
5369 function initMsg() {
5370
5371 // <d>
5372
5373 var options = [],
5374 title,
5375 msg = [],
5376 delimiter = ' + ';
5377
5378 title = 'SoundManager ' + sm2.version + (!sm2.html5Only && sm2.useHTML5Audio ? (sm2.hasHTML5 ? ' + HTML5 audio' : ', no HTML5 audio support') : '');
5379
5380 if (!sm2.html5Only) {
5381
5382 if (sm2.preferFlash) {
5383 options.push('preferFlash');
5384 }
5385
5386 if (sm2.useHighPerformance) {
5387 options.push('useHighPerformance');
5388 }
5389
5390 if (sm2.flashPollingInterval) {
5391 options.push('flashPollingInterval (' + sm2.flashPollingInterval + 'ms)');
5392 }
5393
5394 if (sm2.html5PollingInterval) {
5395 options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)');
5396 }
5397
5398 if (sm2.wmode) {
5399 options.push('wmode (' + sm2.wmode + ')');
5400 }
5401
5402 if (sm2.debugFlash) {
5403 options.push('debugFlash');
5404 }
5405
5406 if (sm2.useFlashBlock) {
5407 options.push('flashBlock');
5408 }
5409
5410 } else if (sm2.html5PollingInterval) {
5411 options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)');
5412 }
5413
5414 if (options.length) {
5415 msg = msg.concat([options.join(delimiter)]);
5416 }
5417
5418 sm2._wD(title + (msg.length ? delimiter + msg.join(', ') : ''), 1);
5419
5420 showSupport();
5421
5422 // </d>
5423
5424 }
5425
5426 if (sm2.html5Only) {
5427
5428 // 100% HTML5 mode
5429 setVersionInfo();
5430
5431 initMsg();
5432 sm2.oMC = id(sm2.movieID);
5433 init();
5434
5435 // prevent multiple init attempts
5436 didAppend = true;
5437
5438 appendSuccess = true;
5439
5440 return false;
5441
5442 }
5443
5444 // flash path
5445 var remoteURL = (movieURL || sm2.url),
5446 localURL = (sm2.altURL || remoteURL),
5447 swfTitle = 'JS/Flash audio component (SoundManager 2)',
5448 oTarget = getDocument(),
5449 extraClass = getSWFCSS(),
5450 isRTL = null,
5451 html = doc.getElementsByTagName('html')[0],
5452 oEmbed, oMovie, tmp, movieHTML, oEl, s, x, sClass;
5453
5454 isRTL = (html && html.dir && html.dir.match(/rtl/i));
5455 movieID = (movieID === _undefined ? sm2.id : movieID);
5456
5457 function param(name, value) {
5458 return '<param name="' + name + '" value="' + value + '" />';
5459 }
5460
5461 // safety check for legacy (change to Flash 9 URL)
5462 setVersionInfo();
5463 sm2.url = normalizeMovieURL(overHTTP ? remoteURL : localURL);
5464 movieURL = sm2.url;
5465
5466 sm2.wmode = (!sm2.wmode && sm2.useHighPerformance ? 'transparent' : sm2.wmode);
5467
5468 if (sm2.wmode !== null && (ua.match(/msie 8/i) || (!isIE && !sm2.useHighPerformance)) && navigator.platform.match(/win32|win64/i)) {
5469 /**
5470 * extra-special case: movie doesn't load until scrolled into view when using wmode = anything but 'window' here
5471 * does not apply when using high performance (position:fixed means on-screen), OR infinite flash load timeout
5472 * wmode breaks IE 8 on Vista + Win7 too in some cases, as of January 2011 (?)
5473 */
5474 messages.push(strings.spcWmode);
5475 sm2.wmode = null;
5476 }
5477
5478 oEmbed = {
5479 name: movieID,
5480 id: movieID,
5481 src: movieURL,
5482 quality: 'high',
5483 allowScriptAccess: sm2.allowScriptAccess,
5484 bgcolor: sm2.bgColor,
5485 pluginspage: http + 'www.macromedia.com/go/getflashplayer',
5486 title: swfTitle,
5487 type: 'application/x-shockwave-flash',
5488 wmode: sm2.wmode,
5489 // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
5490 hasPriority: 'true'
5491 };
5492
5493 if (sm2.debugFlash) {
5494 oEmbed.FlashVars = 'debug=1';
5495 }
5496
5497 if (!sm2.wmode) {
5498 // don't write empty attribute
5499 delete oEmbed.wmode;
5500 }
5501
5502 if (isIE) {
5503
5504 // IE is "special".
5505 oMovie = doc.createElement('div');
5506 movieHTML = [
5507 '<object id="' + movieID + '" data="' + movieURL + '" 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">',
5508 param('movie', movieURL),
5509 param('AllowScriptAccess', sm2.allowScriptAccess),
5510 param('quality', oEmbed.quality),
5511 (sm2.wmode ? param('wmode', sm2.wmode) : ''),
5512 param('bgcolor', sm2.bgColor),
5513 param('hasPriority', 'true'),
5514 (sm2.debugFlash ? param('FlashVars', oEmbed.FlashVars) : ''),
5515 '</object>'
5516 ].join('');
5517
5518 } else {
5519
5520 oMovie = doc.createElement('embed');
5521 for (tmp in oEmbed) {
5522 if (oEmbed.hasOwnProperty(tmp)) {
5523 oMovie.setAttribute(tmp, oEmbed[tmp]);
5524 }
5525 }
5526
5527 }
5528
5529 initDebug();
5530 extraClass = getSWFCSS();
5531 oTarget = getDocument();
5532
5533 if (oTarget) {
5534
5535 sm2.oMC = (id(sm2.movieID) || doc.createElement('div'));
5536
5537 if (!sm2.oMC.id) {
5538
5539 sm2.oMC.id = sm2.movieID;
5540 sm2.oMC.className = swfCSS.swfDefault + ' ' + extraClass;
5541 s = null;
5542 oEl = null;
5543
5544 if (!sm2.useFlashBlock) {
5545 if (sm2.useHighPerformance) {
5546 // on-screen at all times
5547 s = {
5548 position: 'fixed',
5549 width: '8px',
5550 height: '8px',
5551 // >= 6px for flash to run fast, >= 8px to start up under Firefox/win32 in some cases. odd? yes.
5552 bottom: '0px',
5553 left: '0px',
5554 overflow: 'hidden'
5555 };
5556 } else {
5557 // hide off-screen, lower priority
5558 s = {
5559 position: 'absolute',
5560 width: '6px',
5561 height: '6px',
5562 top: '-9999px',
5563 left: '-9999px'
5564 };
5565 if (isRTL) {
5566 s.left = Math.abs(parseInt(s.left, 10)) + 'px';
5567 }
5568 }
5569 }
5570
5571 if (isWebkit) {
5572 // soundcloud-reported render/crash fix, safari 5
5573 sm2.oMC.style.zIndex = 10000;
5574 }
5575
5576 if (!sm2.debugFlash) {
5577 for (x in s) {
5578 if (s.hasOwnProperty(x)) {
5579 sm2.oMC.style[x] = s[x];
5580 }
5581 }
5582 }
5583
5584 try {
5585
5586 if (!isIE) {
5587 sm2.oMC.appendChild(oMovie);
5588 }
5589
5590 oTarget.appendChild(sm2.oMC);
5591
5592 if (isIE) {
5593 oEl = sm2.oMC.appendChild(doc.createElement('div'));
5594 oEl.className = swfCSS.swfBox;
5595 oEl.innerHTML = movieHTML;
5596 }
5597
5598 appendSuccess = true;
5599
5600 } catch(e) {
5601
5602 throw new Error(str('domError') + ' \n' + e.toString());
5603
5604 }
5605
5606 } else {
5607
5608 // SM2 container is already in the document (eg. flashblock use case)
5609 sClass = sm2.oMC.className;
5610 sm2.oMC.className = (sClass ? sClass + ' ' : swfCSS.swfDefault) + (extraClass ? ' ' + extraClass : '');
5611 sm2.oMC.appendChild(oMovie);
5612
5613 if (isIE) {
5614 oEl = sm2.oMC.appendChild(doc.createElement('div'));
5615 oEl.className = swfCSS.swfBox;
5616 oEl.innerHTML = movieHTML;
5617 }
5618
5619 appendSuccess = true;
5620
5621 }
5622
5623 }
5624
5625 didAppend = true;
5626
5627 initMsg();
5628
5629 // sm2._wD(sm + ': Trying to load ' + movieURL + (!overHTTP && sm2.altURL ? ' (alternate URL)' : ''), 1);
5630
5631 return true;
5632
5633 };
5634
5635 initMovie = function() {
5636
5637 if (sm2.html5Only) {
5638 createMovie();
5639 return false;
5640 }
5641
5642 // attempt to get, or create, movie (may already exist)
5643 if (flash) return false;
5644
5645 if (!sm2.url) {
5646
5647 /**
5648 * Something isn't right - we've reached init, but the soundManager url property has not been set.
5649 * User has not called setup({url: ...}), or has not set soundManager.url (legacy use case) directly before init time.
5650 * Notify and exit. If user calls setup() with a url: property, init will be restarted as in the deferred loading case.
5651 */
5652
5653 _wDS('noURL');
5654 return false;
5655
5656 }
5657
5658 // inline markup case
5659 flash = sm2.getMovie(sm2.id);
5660
5661 if (!flash) {
5662
5663 if (!oRemoved) {
5664
5665 // try to create
5666 createMovie(sm2.id, sm2.url);
5667
5668 } else {
5669
5670 // try to re-append removed movie after reboot()
5671 if (!isIE) {
5672 sm2.oMC.appendChild(oRemoved);
5673 } else {
5674 sm2.oMC.innerHTML = oRemovedHTML;
5675 }
5676
5677 oRemoved = null;
5678 didAppend = true;
5679
5680 }
5681
5682 flash = sm2.getMovie(sm2.id);
5683
5684 }
5685
5686 if (typeof sm2.oninitmovie === 'function') {
5687 setTimeout(sm2.oninitmovie, 1);
5688 }
5689
5690 // <d>
5691 flushMessages();
5692 // </d>
5693
5694 return true;
5695
5696 };
5697
5698 delayWaitForEI = function() {
5699
5700 setTimeout(waitForEI, 1000);
5701
5702 };
5703
5704 rebootIntoHTML5 = function() {
5705
5706 // special case: try for a reboot with preferFlash: false, if 100% HTML5 mode is possible and useFlashBlock is not enabled.
5707
5708 window.setTimeout(function() {
5709
5710 complain(smc + 'useFlashBlock is false, 100% HTML5 mode is possible. Rebooting with preferFlash: false...');
5711
5712 sm2.setup({
5713 preferFlash: false
5714 }).reboot();
5715
5716 // if for some reason you want to detect this case, use an ontimeout() callback and look for html5Only and didFlashBlock == true.
5717 sm2.didFlashBlock = true;
5718
5719 sm2.beginDelayedInit();
5720
5721 }, 1);
5722
5723 };
5724
5725 waitForEI = function() {
5726
5727 var p,
5728 loadIncomplete = false;
5729
5730 if (!sm2.url) {
5731 // No SWF url to load (noURL case) - exit for now. Will be retried when url is set.
5732 return;
5733 }
5734
5735 if (waitingForEI) {
5736 return;
5737 }
5738
5739 waitingForEI = true;
5740 event.remove(window, 'load', delayWaitForEI);
5741
5742 if (hasFlash && tryInitOnFocus && !isFocused) {
5743 // Safari won't load flash in background tabs, only when focused.
5744 _wDS('waitFocus');
5745 return;
5746 }
5747
5748 if (!didInit) {
5749 p = sm2.getMoviePercent();
5750 if (p > 0 && p < 100) {
5751 loadIncomplete = true;
5752 }
5753 }
5754
5755 setTimeout(function() {
5756
5757 p = sm2.getMoviePercent();
5758
5759 if (loadIncomplete) {
5760 // special case: if movie *partially* loaded, retry until it's 100% before assuming failure.
5761 waitingForEI = false;
5762 sm2._wD(str('waitSWF'));
5763 window.setTimeout(delayWaitForEI, 1);
5764 return;
5765 }
5766
5767 // <d>
5768 if (!didInit) {
5769
5770 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);
5771
5772 if (!overHTTP && p) {
5773
5774 _wDS('localFail', 2);
5775
5776 if (!sm2.debugFlash) {
5777 _wDS('tryDebug', 2);
5778 }
5779
5780 }
5781
5782 if (p === 0) {
5783
5784 // if 0 (not null), probably a 404.
5785 sm2._wD(str('swf404', sm2.url), 1);
5786
5787 }
5788
5789 debugTS('flashtojs', false, ': Timed out' + (overHTTP ? ' (Check flash security or flash blockers)' : ' (No plugin/missing SWF?)'));
5790
5791 }
5792 // </d>
5793
5794 // give up / time-out, depending
5795
5796 if (!didInit && okToDisable) {
5797
5798 if (p === null) {
5799
5800 // SWF failed to report load progress. Possibly blocked.
5801
5802 if (sm2.useFlashBlock || sm2.flashLoadTimeout === 0) {
5803
5804 if (sm2.useFlashBlock) {
5805
5806 flashBlockHandler();
5807
5808 }
5809
5810 _wDS('waitForever');
5811
5812 } else if (!sm2.useFlashBlock && canIgnoreFlash) {
5813
5814 // no custom flash block handling, but SWF has timed out. Will recover if user unblocks / allows SWF load.
5815 rebootIntoHTML5();
5816
5817 } else {
5818
5819 _wDS('waitForever');
5820
5821 // fire any regular registered ontimeout() listeners.
5822 processOnEvents({
5823 type: 'ontimeout',
5824 ignoreInit: true,
5825 error: {
5826 type: 'INIT_FLASHBLOCK'
5827 }
5828 });
5829
5830 }
5831
5832 } else if (sm2.flashLoadTimeout === 0) {
5833
5834 // SWF loaded? Shouldn't be a blocking issue, then.
5835
5836 _wDS('waitForever');
5837
5838 } else if (!sm2.useFlashBlock && canIgnoreFlash) {
5839
5840 rebootIntoHTML5();
5841
5842 } else {
5843
5844 failSafely(true);
5845
5846 }
5847
5848 }
5849
5850 }, sm2.flashLoadTimeout);
5851
5852 };
5853
5854 handleFocus = function() {
5855
5856 function cleanup() {
5857 event.remove(window, 'focus', handleFocus);
5858 }
5859
5860 if (isFocused || !tryInitOnFocus) {
5861 // already focused, or not special Safari background tab case
5862 cleanup();
5863 return true;
5864 }
5865
5866 okToDisable = true;
5867 isFocused = true;
5868 _wDS('gotFocus');
5869
5870 // allow init to restart
5871 waitingForEI = false;
5872
5873 // kick off ExternalInterface timeout, now that the SWF has started
5874 delayWaitForEI();
5875
5876 cleanup();
5877 return true;
5878
5879 };
5880
5881 flushMessages = function() {
5882
5883 // <d>
5884
5885 // SM2 pre-init debug messages
5886 if (messages.length) {
5887 sm2._wD('SoundManager 2: ' + messages.join(' '), 1);
5888 messages = [];
5889 }
5890
5891 // </d>
5892
5893 };
5894
5895 showSupport = function() {
5896
5897 // <d>
5898
5899 flushMessages();
5900
5901 var item, tests = [];
5902
5903 if (sm2.useHTML5Audio && sm2.hasHTML5) {
5904 for (item in sm2.audioFormats) {
5905 if (sm2.audioFormats.hasOwnProperty(item)) {
5906 tests.push(item + ' = ' + sm2.html5[item] + (!sm2.html5[item] && needsFlash && sm2.flash[item] ? ' (using flash)' : (sm2.preferFlash && sm2.flash[item] && needsFlash ? ' (preferring flash)' : (!sm2.html5[item] ? ' (' + (sm2.audioFormats[item].required ? 'required, ' : '') + 'and no flash support)' : ''))));
5907 }
5908 }
5909 sm2._wD('SoundManager 2 HTML5 support: ' + tests.join(', '), 1);
5910 }
5911
5912 // </d>
5913
5914 };
5915
5916 initComplete = function(bNoDisable) {
5917
5918 if (didInit) return false;
5919
5920 if (sm2.html5Only) {
5921 // all good.
5922 _wDS('sm2Loaded', 1);
5923 didInit = true;
5924 initUserOnload();
5925 debugTS('onload', true);
5926 return true;
5927 }
5928
5929 var wasTimeout = (sm2.useFlashBlock && sm2.flashLoadTimeout && !sm2.getMoviePercent()),
5930 result = true,
5931 error;
5932
5933 if (!wasTimeout) {
5934 didInit = true;
5935 }
5936
5937 error = {
5938 type: (!hasFlash && needsFlash ? 'NO_FLASH' : 'INIT_TIMEOUT')
5939 };
5940
5941 sm2._wD('SoundManager 2 ' + (disabled ? 'failed to load' : 'loaded') + ' (' + (disabled ? 'Flash security/load error' : 'OK') + ') ' + String.fromCharCode(disabled ? 10006 : 10003), disabled ? 2 : 1);
5942
5943 if (disabled || bNoDisable) {
5944
5945 if (sm2.useFlashBlock && sm2.oMC) {
5946 sm2.oMC.className = getSWFCSS() + ' ' + (sm2.getMoviePercent() === null ? swfCSS.swfTimedout : swfCSS.swfError);
5947 }
5948
5949 processOnEvents({
5950 type: 'ontimeout',
5951 error: error,
5952 ignoreInit: true
5953 });
5954
5955 debugTS('onload', false);
5956 catchError(error);
5957
5958 result = false;
5959
5960 } else {
5961
5962 debugTS('onload', true);
5963
5964 }
5965
5966 if (!disabled) {
5967
5968 if (sm2.waitForWindowLoad && !windowLoaded) {
5969
5970 _wDS('waitOnload');
5971 event.add(window, 'load', initUserOnload);
5972
5973 } else {
5974
5975 // <d>
5976 if (sm2.waitForWindowLoad && windowLoaded) {
5977 _wDS('docLoaded');
5978 }
5979 // </d>
5980
5981 initUserOnload();
5982
5983 }
5984
5985 }
5986
5987 return result;
5988
5989 };
5990
5991 /**
5992 * apply top-level setupOptions object as local properties, eg., this.setupOptions.flashVersion -> this.flashVersion (soundManager.flashVersion)
5993 * this maintains backward compatibility, and allows properties to be defined separately for use by soundManager.setup().
5994 */
5995
5996 setProperties = function() {
5997
5998 var i,
5999 o = sm2.setupOptions;
6000
6001 for (i in o) {
6002
6003 if (o.hasOwnProperty(i)) {
6004
6005 // assign local property if not already defined
6006
6007 if (sm2[i] === _undefined) {
6008
6009 sm2[i] = o[i];
6010
6011 } else if (sm2[i] !== o[i]) {
6012
6013 // legacy support: write manually-assigned property (eg., soundManager.url) back to setupOptions to keep things in sync
6014 sm2.setupOptions[i] = sm2[i];
6015
6016 }
6017
6018 }
6019
6020 }
6021
6022 };
6023
6024
6025 init = function() {
6026
6027 // called after onload()
6028
6029 if (didInit) {
6030 _wDS('didInit');
6031 return false;
6032 }
6033
6034 function cleanup() {
6035 event.remove(window, 'load', sm2.beginDelayedInit);
6036 }
6037
6038 if (sm2.html5Only) {
6039
6040 if (!didInit) {
6041 // we don't need no steenking flash!
6042 cleanup();
6043 sm2.enabled = true;
6044 initComplete();
6045 }
6046
6047 return true;
6048
6049 }
6050
6051 // flash path
6052 initMovie();
6053
6054 try {
6055
6056 // attempt to talk to Flash
6057 flash._externalInterfaceTest(false);
6058
6059 /**
6060 * Apply user-specified polling interval, OR, if "high performance" set, faster vs. default polling
6061 * (determines frequency of whileloading/whileplaying callbacks, effectively driving UI framerates)
6062 */
6063 setPolling(true, (sm2.flashPollingInterval || (sm2.useHighPerformance ? 10 : 50)));
6064
6065 if (!sm2.debugMode) {
6066 // stop the SWF from making debug output calls to JS
6067 flash._disableDebug();
6068 }
6069
6070 sm2.enabled = true;
6071 debugTS('jstoflash', true);
6072
6073 if (!sm2.html5Only) {
6074 // prevent browser from showing cached page state (or rather, restoring "suspended" page state) via back button, because flash may be dead
6075 // http://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
6076 event.add(window, 'unload', doNothing);
6077 }
6078
6079 } catch(e) {
6080
6081 sm2._wD('js/flash exception: ' + e.toString());
6082
6083 debugTS('jstoflash', false);
6084
6085 catchError({
6086 type: 'JS_TO_FLASH_EXCEPTION',
6087 fatal: true
6088 });
6089
6090 // don't disable, for reboot()
6091 failSafely(true);
6092
6093 initComplete();
6094
6095 return false;
6096
6097 }
6098
6099 initComplete();
6100
6101 // disconnect events
6102 cleanup();
6103
6104 return true;
6105
6106 };
6107
6108 domContentLoaded = function() {
6109
6110 if (didDCLoaded) return false;
6111
6112 didDCLoaded = true;
6113
6114 // assign top-level soundManager properties eg. soundManager.url
6115 setProperties();
6116
6117 initDebug();
6118
6119 if (!hasFlash && sm2.hasHTML5) {
6120
6121 sm2._wD('SoundManager 2: No Flash detected' + (!sm2.useHTML5Audio ? ', enabling HTML5.' : '. Trying HTML5-only mode.'), 1);
6122
6123 sm2.setup({
6124 useHTML5Audio: true,
6125 // make sure we aren't preferring flash, either
6126 // TODO: preferFlash should not matter if flash is not installed. Currently, stuff breaks without the below tweak.
6127 preferFlash: false
6128 });
6129
6130 }
6131
6132 testHTML5();
6133
6134 if (!hasFlash && needsFlash) {
6135
6136 messages.push(strings.needFlash);
6137
6138 // TODO: Fatal here vs. timeout approach, etc.
6139 // hack: fail sooner.
6140 sm2.setup({
6141 flashLoadTimeout: 1
6142 });
6143
6144 }
6145
6146 if (doc.removeEventListener) {
6147 doc.removeEventListener('DOMContentLoaded', domContentLoaded, false);
6148 }
6149
6150 initMovie();
6151
6152 return true;
6153
6154 };
6155
6156 domContentLoadedIE = function() {
6157
6158 if (doc.readyState === 'complete') {
6159 domContentLoaded();
6160 doc.detachEvent('onreadystatechange', domContentLoadedIE);
6161 }
6162
6163 return true;
6164
6165 };
6166
6167 winOnLoad = function() {
6168
6169 // catch edge case of initComplete() firing after window.load()
6170 windowLoaded = true;
6171
6172 // catch case where DOMContentLoaded has been sent, but we're still in doc.readyState = 'interactive'
6173 domContentLoaded();
6174
6175 event.remove(window, 'load', winOnLoad);
6176
6177 };
6178
6179 // sniff up-front
6180 detectFlash();
6181
6182 // focus and window load, init (primarily flash-driven)
6183 event.add(window, 'focus', handleFocus);
6184 event.add(window, 'load', delayWaitForEI);
6185 event.add(window, 'load', winOnLoad);
6186
6187 if (doc.addEventListener) {
6188
6189 doc.addEventListener('DOMContentLoaded', domContentLoaded, false);
6190
6191 } else if (doc.attachEvent) {
6192
6193 doc.attachEvent('onreadystatechange', domContentLoadedIE);
6194
6195 } else {
6196
6197 // no add/attachevent support - safe to assume no JS -> Flash either
6198 debugTS('onload', false);
6199 catchError({
6200 type: 'NO_DOM2_EVENTS',
6201 fatal: true
6202 });
6203
6204 }
6205
6206} // SoundManager()
6207
6208// SM2_DEFER details: http://www.schillmania.com/projects/soundmanager2/doc/getstarted/#lazy-loading
6209
6210if (window.SM2_DEFER === _undefined || !SM2_DEFER) {
6211 soundManager = new SoundManager();
6212}
6213
6214/**
6215 * SoundManager public interfaces
6216 * ------------------------------
6217 */
6218
6219if (typeof module === 'object' && module && typeof module.exports === 'object') {
6220
6221 /**
6222 * commonJS module
6223 */
6224
6225 module.exports.SoundManager = SoundManager;
6226 module.exports.soundManager = soundManager;
6227
6228} else if (typeof define === 'function' && define.amd) {
6229
6230 /**
6231 * AMD - requireJS
6232 * basic usage:
6233 * require(["/path/to/soundmanager2.js"], function(SoundManager) {
6234 * SoundManager.getInstance().setup({
6235 * url: '/swf/',
6236 * onready: function() { ... }
6237 * })
6238 * });
6239 *
6240 * SM2_DEFER usage:
6241 * window.SM2_DEFER = true;
6242 * require(["/path/to/soundmanager2.js"], function(SoundManager) {
6243 * SoundManager.getInstance(function() {
6244 * var soundManager = new SoundManager.constructor();
6245 * soundManager.setup({
6246 * url: '/swf/',
6247 * ...
6248 * });
6249 * ...
6250 * soundManager.beginDelayedInit();
6251 * return soundManager;
6252 * })
6253 * });
6254 */
6255
6256 define(function() {
6257 /**
6258 * Retrieve the global instance of SoundManager.
6259 * If a global instance does not exist it can be created using a callback.
6260 *
6261 * @param {Function} smBuilder Optional: Callback used to create a new SoundManager instance
6262 * @return {SoundManager} The global SoundManager instance
6263 */
6264 function getInstance(smBuilder) {
6265 if (!window.soundManager && smBuilder instanceof Function) {
6266 var instance = smBuilder(SoundManager);
6267 if (instance instanceof SoundManager) {
6268 window.soundManager = instance;
6269 }
6270 }
6271 return window.soundManager;
6272 }
6273 return {
6274 constructor: SoundManager,
6275 getInstance: getInstance
6276 };
6277 });
6278
6279}
6280
6281// standard browser case
6282
6283// constructor
6284window.SoundManager = SoundManager;
6285
6286/**
6287 * note: SM2 requires a window global due to Flash, which makes calls to window.soundManager.
6288 * Flash may not always be needed, but this is not known until async init and SM2 may even "reboot" into Flash mode.
6289 */
6290
6291// public API, flash callbacks etc.
6292window.soundManager = soundManager;
6293
6294}(window));</script>
6295
6296
6297
6298
6299
6300<script>(function(window) {
6301
6302 /**
6303 * SoundManager 2: "Bar UI" player
6304 * Copyright (c) 2014, Scott Schiller. All rights reserved.
6305 * http://www.schillmania.com/projects/soundmanager2/
6306 * Code provided under BSD license.
6307 * http://schillmania.com/projects/soundmanager2/license.txt
6308 */
6309
6310 /* global console, document, navigator, soundManager, window */
6311
6312 'use strict';
6313
6314 var Player,
6315 players = [],
6316 // CSS selector that will get us the top-level DOM node for the player UI.
6317 playerSelector = '.sm2-bar-ui',
6318 playerOptions,
6319 utils;
6320
6321 /**
6322 * The following are player object event callback examples.
6323 * Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc.
6324 * soundObject is provided for whileplaying() etc., but playback control should be done via the player object.
6325 */
6326 players.on = {
6327 /*
6328 play: function(player, soundObject) {
6329 console.log('playing', player);
6330 },
6331 whileplaying: function(player, soundObject) {
6332 console.log('whileplaying', player, soundObject);
6333 },
6334 finish: function(player, soundObject) {
6335 // each sound
6336 console.log('finish', player);
6337 },
6338 pause: function(player, soundObject) {
6339 console.log('pause', player);
6340 },
6341 error: function(player, soundObject) {
6342 console.log('error', player);
6343 },
6344 end: function(player, soundObject) {
6345 // end of playlist
6346 console.log('end', player);
6347 }
6348 */
6349 };
6350
6351 playerOptions = {
6352 // useful when multiple players are in use, or other SM2 sounds are active etc.
6353 stopOtherSounds: true,
6354 // CSS class to let the browser load the URL directly e.g., <a href="foo.mp3" class="sm2-exclude">download foo.mp3</a>
6355 excludeClass: 'sm2-exclude'
6356 };
6357
6358 soundManager.setup({
6359 // trade-off: higher UI responsiveness (play/progress bar), but may use more CPU.
6360 html5PollingInterval: 50,
6361 flashVersion: 9
6362 });
6363
6364 soundManager.onready(function() {
6365
6366 var nodes, i, j;
6367
6368 nodes = utils.dom.getAll(playerSelector);
6369
6370 if (nodes && nodes.length) {
6371 for (i = 0, j = nodes.length; i < j; i++) {
6372 players.push(new Player(nodes[i]));
6373 }
6374 }
6375
6376 });
6377
6378 /**
6379 * player bits
6380 */
6381
6382 Player = function(playerNode) {
6383
6384 var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports;
6385
6386 css = {
6387 disabled: 'disabled',
6388 selected: 'selected',
6389 active: 'active',
6390 legacy: 'legacy',
6391 noVolume: 'no-volume',
6392 playlistOpen: 'playlist-open'
6393 };
6394
6395 dom = {
6396 o: null,
6397 playlist: null,
6398 playlistTarget: null,
6399 playlistContainer: null,
6400 time: null,
6401 player: null,
6402 progress: null,
6403 progressTrack: null,
6404 progressBar: null,
6405 duration: null,
6406 volume: null
6407 };
6408
6409 // prepended to tracks when a sound fails to load/play
6410 extras = {
6411 loadFailedCharacter: '<span title="Failed to load/play." class="load-error">✖</span>'
6412 };
6413
6414 function stopOtherSounds() {
6415
6416 if (playerOptions.stopOtherSounds) {
6417 soundManager.stopAll();
6418 }
6419
6420 }
6421
6422 function callback(method, oSound) {
6423 if (method) {
6424 // fire callback, passing current player and sound objects
6425 if (exports.on && exports.on[method]) {
6426 exports.on[method](exports, oSound);
6427 } else if (players.on[method]) {
6428 players.on[method](exports, oSound);
6429 }
6430 }
6431 }
6432
6433 function getTime(msec, useString) {
6434
6435 // convert milliseconds to hh:mm:ss, return as object literal or string
6436
6437 var nSec = Math.floor(msec / 1000),
6438 hh = Math.floor(nSec / 3600),
6439 min = Math.floor(nSec / 60) - Math.floor(hh * 60),
6440 sec = Math.floor(nSec - (hh * 3600) - (min * 60));
6441
6442 // if (min === 0 && sec === 0) return null; // return 0:00 as null
6443
6444 return (useString ? ((hh ? hh + ':' : '') + (hh && min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)) : { min: min, sec: sec });
6445
6446 }
6447
6448 function setTitle(item) {
6449
6450 // given a link, update the "now playing" UI.
6451
6452 // if this is an <li> with an inner link, grab and use the text from that.
6453 var links = item.getElementsByTagName('a');
6454
6455 if (links.length) {
6456 item = links[0];
6457 }
6458
6459 // remove any failed character sequence, also
6460 dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '</li></ul>';
6461
6462 if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) {
6463 // this item can use <marquee>, in fact.
6464 dom.playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li><marquee>' + item.innerHTML + '</marquee></li></ul>';
6465 }
6466
6467 }
6468
6469 function makeSound(url) {
6470
6471 var sound = soundManager.createSound({
6472
6473 url: url,
6474
6475 volume: defaultVolume,
6476
6477 whileplaying: function() {
6478
6479 var progressMaxLeft = 100,
6480 left,
6481 width;
6482
6483 left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%';
6484 width = Math.min(100, Math.max(0, (100 * (this.position / this.durationEstimate)))) + '%';
6485
6486 if (this.duration) {
6487
6488 dom.progress.style.left = left;
6489 dom.progressBar.style.width = width;
6490
6491 // TODO: only write changes
6492 dom.time.innerHTML = getTime(this.position, true);
6493
6494 }
6495
6496 callback('whileplaying', this);
6497
6498 },
6499
6500 onbufferchange: function(isBuffering) {
6501
6502 if (isBuffering) {
6503 utils.css.add(dom.o, 'buffering');
6504 } else {
6505 utils.css.remove(dom.o, 'buffering');
6506 }
6507
6508 },
6509
6510 onplay: function() {
6511 utils.css.swap(dom.o, 'paused', 'playing');
6512 callback('play', this);
6513 },
6514
6515 onpause: function() {
6516 utils.css.swap(dom.o, 'playing', 'paused');
6517 callback('pause', this);
6518 },
6519
6520 onresume: function() {
6521 utils.css.swap(dom.o, 'paused', 'playing');
6522 },
6523
6524 whileloading: function() {
6525
6526 if (!this.isHTML5) {
6527 dom.duration.innerHTML = getTime(this.durationEstimate, true);
6528 }
6529
6530 },
6531
6532 onload: function(ok) {
6533
6534 if (ok) {
6535
6536 dom.duration.innerHTML = getTime(this.duration, true);
6537
6538 } else if (this._iO && this._iO.onerror) {
6539
6540 this._iO.onerror();
6541
6542 }
6543
6544 },
6545
6546 onerror: function() {
6547
6548 // sound failed to load.
6549 var item, element, html;
6550
6551 item = playlistController.getItem();
6552
6553 if (item) {
6554
6555 // note error, delay 2 seconds and advance?
6556 // playlistTarget.innerHTML = '<ul class="sm2-playlist-bd"><li>' + item.innerHTML + '</li></ul>';
6557
6558 if (extras.loadFailedCharacter) {
6559 dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('<li>', '<li>' + extras.loadFailedCharacter + ' ');
6560 if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) {
6561 element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0];
6562 html = element.innerHTML;
6563 if (html.indexOf(extras.loadFailedCharacter) === -1) {
6564 element.innerHTML = extras.loadFailedCharacter + ' ' + html;
6565 }
6566 }
6567 }
6568
6569 }
6570
6571 callback('error', this);
6572
6573 // load next, possibly with delay.
6574
6575 if (navigator.userAgent.match(/mobile/i)) {
6576 // mobile will likely block the next play() call if there is a setTimeout() - so don't use one here.
6577 actions.next();
6578 } else {
6579 if (playlistController.data.timer) {
6580 window.clearTimeout(playlistController.data.timer);
6581 }
6582 playlistController.data.timer = window.setTimeout(actions.next, 2000);
6583 }
6584
6585 },
6586
6587 onstop: function() {
6588
6589 utils.css.remove(dom.o, 'playing');
6590
6591 },
6592
6593 onfinish: function() {
6594
6595 var lastIndex, item;
6596
6597 utils.css.remove(dom.o, 'playing');
6598
6599 dom.progress.style.left = '0%';
6600
6601 lastIndex = playlistController.data.selectedIndex;
6602
6603 callback('finish', this);
6604
6605 // next track?
6606 item = playlistController.getNext();
6607
6608 // don't play the same item over and over again, if at end of playlist (excluding single item case.)
6609 if (item && (playlistController.data.selectedIndex !== lastIndex || (playlistController.data.playlist.length === 1 && playlistController.data.loopMode))) {
6610
6611 playlistController.select(item);
6612
6613 setTitle(item);
6614
6615 stopOtherSounds();
6616
6617 // play next
6618 this.play({
6619 url: playlistController.getURL()
6620 });
6621
6622 } else {
6623
6624 // end of playlist case
6625
6626 // explicitly stop?
6627 // this.stop();
6628
6629 callback('end', this);
6630
6631 }
6632
6633 }
6634
6635 });
6636
6637 return sound;
6638
6639 }
6640
6641 function playLink(link) {
6642
6643 // if a link is OK, play it.
6644
6645 if (soundManager.canPlayURL(link.href)) {
6646
6647 // if there's a timer due to failure to play one track, cancel it.
6648 // catches case when user may use previous/next after an error.
6649 if (playlistController.data.timer) {
6650 window.clearTimeout(playlistController.data.timer);
6651 playlistController.data.timer = null;
6652 }
6653
6654 if (!soundObject) {
6655 soundObject = makeSound(link.href);
6656 }
6657
6658 // required to reset pause/play state on iOS so whileplaying() works? odd.
6659 soundObject.stop();
6660
6661 playlistController.select(link.parentNode);
6662
6663 setTitle(link.parentNode);
6664
6665 // reset the UI
6666 // TODO: function that also resets/hides timing info.
6667 dom.progress.style.left = '0px';
6668 dom.progressBar.style.width = '0px';
6669
6670 stopOtherSounds();
6671
6672 soundObject.play({
6673 url: link.href,
6674 position: 0
6675 });
6676
6677 }
6678
6679 }
6680
6681 function PlaylistController() {
6682
6683 var data;
6684
6685 data = {
6686
6687 // list of nodes?
6688 playlist: [],
6689
6690 // NOTE: not implemented yet.
6691 // shuffledIndex: [],
6692 // shuffleMode: false,
6693
6694 // selection
6695 selectedIndex: 0,
6696
6697 loopMode: false,
6698
6699 timer: null
6700
6701 };
6702
6703 function getPlaylist() {
6704
6705 return data.playlist;
6706
6707 }
6708
6709 function getItem(offset) {
6710
6711 var list,
6712 item;
6713
6714 // given the current selection (or an offset), return the current item.
6715
6716 // if currently null, may be end of list case. bail.
6717 if (data.selectedIndex === null) {
6718 return offset;
6719 }
6720
6721 list = getPlaylist();
6722
6723 // use offset if provided, otherwise take default selected.
6724 offset = (offset !== undefined ? offset : data.selectedIndex);
6725
6726 // safety check - limit to between 0 and list length
6727 offset = Math.max(0, Math.min(offset, list.length));
6728
6729 item = list[offset];
6730
6731 return item;
6732
6733 }
6734
6735 function findOffsetFromItem(item) {
6736
6737 // given an <li> item, find it in the playlist array and return the index.
6738 var list,
6739 i,
6740 j,
6741 offset;
6742
6743 offset = -1;
6744
6745 list = getPlaylist();
6746
6747 if (list) {
6748
6749 for (i = 0, j = list.length; i < j; i++) {
6750 if (list[i] === item) {
6751 offset = i;
6752 break;
6753 }
6754 }
6755
6756 }
6757
6758 return offset;
6759
6760 }
6761
6762 function getNext() {
6763
6764 // don't increment if null.
6765 if (data.selectedIndex !== null) {
6766 data.selectedIndex++;
6767 }
6768
6769 if (data.playlist.length > 1) {
6770
6771 if (data.selectedIndex >= data.playlist.length) {
6772
6773 if (data.loopMode) {
6774
6775 // loop to beginning
6776 data.selectedIndex = 0;
6777
6778 } else {
6779
6780 // no change
6781 data.selectedIndex--;
6782
6783 // end playback
6784 // data.selectedIndex = null;
6785
6786 }
6787
6788 }
6789
6790 } else {
6791
6792 data.selectedIndex = null;
6793
6794 }
6795
6796 return getItem();
6797
6798 }
6799
6800 function getPrevious() {
6801
6802 data.selectedIndex--;
6803
6804 if (data.selectedIndex < 0) {
6805 // wrapping around beginning of list? loop or exit.
6806 if (data.loopMode) {
6807 data.selectedIndex = data.playlist.length - 1;
6808 } else {
6809 // undo
6810 data.selectedIndex++;
6811 }
6812 }
6813
6814 return getItem();
6815
6816 }
6817
6818 function resetLastSelected() {
6819
6820 // remove UI highlight(s) on selected items.
6821 var items,
6822 i, j;
6823
6824 items = utils.dom.getAll(dom.playlist, '.' + css.selected);
6825
6826 for (i = 0, j = items.length; i < j; i++) {
6827 utils.css.remove(items[i], css.selected);
6828 }
6829
6830 }
6831
6832 function select(item) {
6833
6834 var offset,
6835 itemTop,
6836 itemBottom,
6837 containerHeight,
6838 scrollTop,
6839 itemPadding,
6840 liElement;
6841
6842 // remove last selected, if any
6843 resetLastSelected();
6844
6845 if (item) {
6846
6847 liElement = utils.dom.ancestor('li', item);
6848
6849 utils.css.add(liElement, css.selected);
6850
6851 itemTop = item.offsetTop;
6852 itemBottom = itemTop + item.offsetHeight;
6853 containerHeight = dom.playlistContainer.offsetHeight;
6854 scrollTop = dom.playlist.scrollTop;
6855 itemPadding = 8;
6856
6857 if (itemBottom > containerHeight + scrollTop) {
6858 // bottom-align
6859 dom.playlist.scrollTop = (itemBottom - containerHeight) + itemPadding;
6860 } else if (itemTop < scrollTop) {
6861 // top-align
6862 dom.playlist.scrollTop = item.offsetTop - itemPadding;
6863 }
6864
6865 }
6866
6867 // update selected offset, too.
6868 offset = findOffsetFromItem(liElement);
6869
6870 data.selectedIndex = offset;
6871
6872 }
6873
6874 function playItemByOffset(offset) {
6875
6876 var item;
6877
6878 offset = (offset || 0);
6879
6880 item = getItem(offset);
6881
6882 if (item) {
6883 playLink(item.getElementsByTagName('a')[0]);
6884 }
6885
6886 }
6887
6888 function getURL() {
6889
6890 // return URL of currently-selected item
6891 var item, url;
6892
6893 item = getItem();
6894
6895 if (item) {
6896 url = item.getElementsByTagName('a')[0].href;
6897 }
6898
6899 return url;
6900
6901 }
6902
6903 function refreshDOM() {
6904
6905 // get / update playlist from DOM
6906
6907 if (!dom.playlist) {
6908 if (window.console && console.warn) {
6909 console.warn('refreshDOM(): playlist node not found?');
6910 }
6911 return;
6912 }
6913
6914 data.playlist = dom.playlist.getElementsByTagName('li');
6915
6916 }
6917
6918 function initDOM() {
6919
6920 dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target');
6921 dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer');
6922 dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd');
6923
6924 }
6925
6926 function initPlaylistController() {
6927
6928 // inherit the default SM2 volume
6929 defaultVolume = soundManager.defaultOptions.volume;
6930
6931 initDOM();
6932 refreshDOM();
6933
6934 // animate playlist open, if HTML classname indicates so.
6935 if (utils.css.has(dom.o, css.playlistOpen)) {
6936 // hackish: run this after API has returned
6937 window.setTimeout(function() {
6938 actions.menu(true);
6939 }, 1);
6940 }
6941
6942 }
6943
6944 initPlaylistController();
6945
6946 return {
6947 data: data,
6948 refresh: refreshDOM,
6949 getNext: getNext,
6950 getPrevious: getPrevious,
6951 getItem: getItem,
6952 getURL: getURL,
6953 playItemByOffset: playItemByOffset,
6954 select: select
6955 };
6956
6957 }
6958
6959 function isRightClick(e) {
6960
6961 // only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click.
6962 if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) {
6963 // http://www.quirksmode.org/js/events_properties.html#button
6964 return true;
6965 }
6966
6967 return false;
6968
6969 }
6970
6971 function getActionData(target) {
6972
6973 // DOM measurements for volume slider
6974
6975 if (!target) {
6976 return;
6977 }
6978
6979 actionData.volume.x = utils.position.getOffX(target);
6980 actionData.volume.y = utils.position.getOffY(target);
6981
6982 actionData.volume.width = target.offsetWidth;
6983 actionData.volume.height = target.offsetHeight;
6984
6985 // potentially dangerous: this should, but may not be a percentage-based value.
6986 actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10);
6987
6988 // IE gives pixels even if background-size specified as % in CSS. Boourns.
6989 if (window.navigator.userAgent.match(/msie|trident/i)) {
6990 actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100;
6991 }
6992
6993 }
6994
6995 function handleMouseDown(e) {
6996
6997 var links,
6998 target;
6999
7000 target = e.target || e.srcElement;
7001
7002 if (isRightClick(e)) {
7003 return;
7004 }
7005
7006 // normalize to <a>, if applicable.
7007 if (target.nodeName.toLowerCase() !== 'a') {
7008
7009 links = target.getElementsByTagName('a');
7010 if (links && links.length) {
7011 target = target.getElementsByTagName('a')[0];
7012 }
7013
7014 }
7015
7016 if (utils.css.has(target, 'sm2-volume-control')) {
7017
7018 // drag case for volume
7019
7020 getActionData(target);
7021
7022 utils.events.add(document, 'mousemove', actions.adjustVolume);
7023 utils.events.add(document, 'touchmove', actions.adjustVolume);
7024 utils.events.add(document, 'mouseup', actions.releaseVolume);
7025 utils.events.add(document, 'touchend', actions.releaseVolume);
7026
7027 // and apply right away
7028 actions.adjustVolume(e);
7029
7030 }
7031
7032 }
7033
7034 function handleMouse(e) {
7035
7036 var target, barX, barWidth, x, clientX, newPosition, sound;
7037
7038 target = dom.progressTrack;
7039
7040 barX = utils.position.getOffX(target);
7041 barWidth = target.offsetWidth;
7042 clientX = utils.events.getClientX(e);
7043
7044 x = (clientX - barX);
7045
7046 newPosition = (x / barWidth);
7047
7048 sound = soundObject;
7049
7050 if (sound && sound.duration) {
7051
7052 sound.setPosition(sound.duration * newPosition);
7053
7054 // a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet.
7055 if (sound._iO && sound._iO.whileplaying) {
7056 sound._iO.whileplaying.apply(sound);
7057 }
7058
7059 }
7060
7061 if (e.preventDefault) {
7062 e.preventDefault();
7063 }
7064
7065 return false;
7066
7067 }
7068
7069 function releaseMouse(e) {
7070
7071 utils.events.remove(document, 'mousemove', handleMouse);
7072 utils.events.remove(document, 'touchmove', handleMouse);
7073
7074 utils.css.remove(dom.o, 'grabbing');
7075
7076 utils.events.remove(document, 'mouseup', releaseMouse);
7077 utils.events.remove(document, 'touchend', releaseMouse);
7078
7079 utils.events.preventDefault(e);
7080
7081 return false;
7082
7083 }
7084
7085 function handleProgressMouseDown(e) {
7086
7087 if (isRightClick(e)) {
7088 return;
7089 }
7090
7091 utils.css.add(dom.o, 'grabbing');
7092
7093 utils.events.add(document, 'mousemove', handleMouse);
7094 utils.events.add(document, 'touchmove', handleMouse);
7095 utils.events.add(document, 'mouseup', releaseMouse);
7096 utils.events.add(document, 'touchend', releaseMouse);
7097
7098 handleMouse(e);
7099
7100 }
7101
7102 function handleClick(e) {
7103
7104 var evt,
7105 target,
7106 offset,
7107 targetNodeName,
7108 methodName,
7109 href,
7110 handled;
7111
7112 evt = (e || window.event);
7113
7114 target = evt.target || evt.srcElement;
7115
7116 if (target && target.nodeName) {
7117
7118 targetNodeName = target.nodeName.toLowerCase();
7119
7120 if (targetNodeName !== 'a') {
7121
7122 // old IE (IE 8) might return nested elements inside the <a>, eg., <b> etc. Try to find the parent <a>.
7123
7124 if (target.parentNode) {
7125
7126 do {
7127 target = target.parentNode;
7128 targetNodeName = target.nodeName.toLowerCase();
7129 } while (targetNodeName !== 'a' && target.parentNode);
7130
7131 if (!target) {
7132 // something went wrong. bail.
7133 return false;
7134 }
7135
7136 }
7137
7138 }
7139
7140 if (targetNodeName === 'a') {
7141
7142 // yep, it's a link.
7143
7144 href = target.href;
7145
7146 if (soundManager.canPlayURL(href)) {
7147
7148 // not excluded
7149 if (!utils.css.has(target, playerOptions.excludeClass)) {
7150
7151 // find this in the playlist
7152
7153 playLink(target);
7154
7155 handled = true;
7156
7157 }
7158
7159 } else {
7160
7161 // is this one of the action buttons, eg., play/pause, volume, etc.?
7162 offset = target.href.lastIndexOf('#');
7163
7164 if (offset !== -1) {
7165
7166 methodName = target.href.substr(offset + 1);
7167
7168 if (methodName && actions[methodName]) {
7169 handled = true;
7170 actions[methodName](e);
7171 }
7172
7173 }
7174
7175 }
7176
7177 // fall-through case
7178
7179 if (handled) {
7180 // prevent browser fall-through
7181 return utils.events.preventDefault(evt);
7182 }
7183
7184 }
7185
7186 }
7187
7188 return true;
7189
7190 }
7191
7192 function init() {
7193
7194 // init DOM?
7195
7196 if (!playerNode && window.console && console.warn) {
7197 console.warn('init(): No playerNode element?');
7198 }
7199
7200 dom.o = playerNode;
7201
7202 // are we dealing with a crap browser? apply legacy CSS if so.
7203 if (window.navigator.userAgent.match(/msie [678]/i)) {
7204 utils.css.add(dom.o, css.legacy);
7205 }
7206
7207 if (window.navigator.userAgent.match(/mobile/i)) {
7208 // majority of mobile devices don't let HTML5 audio set volume.
7209 utils.css.add(dom.o, css.noVolume);
7210 }
7211
7212 dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball');
7213
7214 dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track');
7215
7216 dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar');
7217
7218 dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control');
7219
7220 // measure volume control dimensions
7221 if (dom.volume) {
7222 getActionData(dom.volume);
7223 }
7224
7225 dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration');
7226
7227 dom.time = utils.dom.get(dom.o, '.sm2-inline-time');
7228
7229 playlistController = new PlaylistController();
7230
7231 defaultItem = playlistController.getItem(0);
7232
7233 playlistController.select(defaultItem);
7234
7235 if (defaultItem) {
7236 setTitle(defaultItem);
7237 }
7238
7239 utils.events.add(dom.o, 'mousedown', handleMouseDown);
7240 utils.events.add(dom.o, 'touchstart', handleMouseDown);
7241 utils.events.add(dom.o, 'click', handleClick);
7242 utils.events.add(dom.progressTrack, 'mousedown', handleProgressMouseDown);
7243 utils.events.add(dom.progressTrack, 'touchstart', handleProgressMouseDown);
7244
7245 }
7246
7247 // ---
7248
7249 actionData = {
7250
7251 volume: {
7252 x: 0,
7253 y: 0,
7254 width: 0,
7255 height: 0,
7256 backgroundSize: 0
7257 }
7258
7259 };
7260
7261 actions = {
7262
7263 play: function(offsetOrEvent) {
7264
7265 /**
7266 * This is an overloaded function that takes mouse/touch events or offset-based item indices.
7267 * Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event.
7268 * If you have the link but not the offset, you can also pass a fake event object with a target of an <a> inside the playlist - e.g. { target: someMP3Link }
7269 */
7270
7271 var target,
7272 href,
7273 e;
7274
7275 if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) {
7276 // smells like a number.
7277 playlistController.playItemByOffset(offsetOrEvent);
7278 return;
7279 }
7280
7281 // DRY things a bit
7282 e = offsetOrEvent;
7283
7284 if (e && e.target) {
7285
7286 target = e.target || e.srcElement;
7287
7288 href = target.href;
7289
7290 }
7291
7292 // haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist
7293 if (!href || href.indexOf('#') !== -1) {
7294 href = dom.playlist.getElementsByTagName('a')[0].href;
7295 }
7296
7297 if (!soundObject) {
7298 soundObject = makeSound(href);
7299 }
7300
7301 // edge case: if the current sound is not playing, stop all others.
7302 if (!soundObject.playState) {
7303 stopOtherSounds();
7304 }
7305
7306 // TODO: if user pauses + unpauses a sound that had an error, try to play next?
7307 soundObject.togglePause();
7308
7309 // special case: clear "play next" timeout, if one exists.
7310 // edge case: user pauses after a song failed to load.
7311 if (soundObject.paused && playlistController.data.timer) {
7312 window.clearTimeout(playlistController.data.timer);
7313 playlistController.data.timer = null;
7314 }
7315
7316 },
7317
7318 pause: function() {
7319
7320 if (soundObject && soundObject.readyState) {
7321 soundObject.pause();
7322 }
7323
7324 },
7325
7326 resume: function() {
7327
7328 if (soundObject && soundObject.readyState) {
7329 soundObject.resume();
7330 }
7331
7332 },
7333
7334 stop: function() {
7335
7336 // just an alias for pause, really.
7337 // don't actually stop because that will mess up some UI state, i.e., dragging the slider.
7338 return actions.pause();
7339
7340 },
7341
7342 next: function(/* e */) {
7343
7344 var item, lastIndex;
7345
7346 // special case: clear "play next" timeout, if one exists.
7347 if (playlistController.data.timer) {
7348 window.clearTimeout(playlistController.data.timer);
7349 playlistController.data.timer = null;
7350 }
7351
7352 lastIndex = playlistController.data.selectedIndex;
7353
7354 item = playlistController.getNext(true);
7355
7356 // don't play the same item again
7357 if (item && playlistController.data.selectedIndex !== lastIndex) {
7358 playLink(item.getElementsByTagName('a')[0]);
7359 }
7360
7361 },
7362
7363 prev: function(/* e */) {
7364
7365 var item, lastIndex;
7366
7367 lastIndex = playlistController.data.selectedIndex;
7368
7369 item = playlistController.getPrevious();
7370
7371 // don't play the same item again
7372 if (item && playlistController.data.selectedIndex !== lastIndex) {
7373 playLink(item.getElementsByTagName('a')[0]);
7374 }
7375
7376 },
7377
7378 shuffle: function(e) {
7379
7380 // NOTE: not implemented yet.
7381
7382 var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle'));
7383
7384 if (target && !utils.css.has(target, css.disabled)) {
7385 utils.css.toggle(target.parentNode, css.active);
7386 playlistController.data.shuffleMode = !playlistController.data.shuffleMode;
7387 }
7388
7389 },
7390
7391 repeat: function(e) {
7392
7393 var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat'));
7394
7395 if (target && !utils.css.has(target, css.disabled)) {
7396 utils.css.toggle(target.parentNode, css.active);
7397 playlistController.data.loopMode = !playlistController.data.loopMode;
7398 }
7399
7400 },
7401
7402 menu: function(ignoreToggle) {
7403
7404 var isOpen;
7405
7406 isOpen = utils.css.has(dom.o, css.playlistOpen);
7407
7408 // hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens.
7409 if (playlistController && !playlistController.data.selectedIndex && !firstOpen) {
7410 dom.playlist.scrollTop = 0;
7411 firstOpen = true;
7412 }
7413
7414 // sniff out booleans from mouse events, as this is referenced directly by event handlers.
7415 if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) {
7416
7417 if (!isOpen) {
7418 // explicitly set height:0, so the first closed -> open animation runs properly
7419 dom.playlistContainer.style.height = '0px';
7420 }
7421
7422 isOpen = utils.css.toggle(dom.o, css.playlistOpen);
7423
7424 }
7425
7426 // playlist
7427 dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px';
7428
7429 },
7430
7431 adjustVolume: function(e) {
7432
7433 /**
7434 * NOTE: this is the mousemove() event handler version.
7435 * Use setVolume(50), etc., to assign volume directly.
7436 */
7437
7438 var backgroundMargin,
7439 pixelMargin,
7440 target,
7441 value,
7442 volume;
7443
7444 value = 0;
7445
7446 target = dom.volume;
7447
7448 // safety net
7449 if (e === undefined) {
7450 return false;
7451 }
7452
7453 // normalize between mouse and touch events
7454 var clientX = utils.events.getClientX(e);
7455
7456 if (!e || clientX === undefined) {
7457 // called directly or with a non-mouseEvent object, etc.
7458 // proxy to the proper method.
7459 if (arguments.length && window.console && window.console.warn) {
7460 console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').');
7461 }
7462 return actions.setVolume.apply(this, arguments);
7463 }
7464
7465 // based on getStyle() result
7466 // figure out spacing around background image based on background size, eg. 60% background size.
7467 // 60% wide means 20% margin on each side.
7468 backgroundMargin = (100 - actionData.volume.backgroundSize) / 2;
7469
7470 // relative position of mouse over element
7471 value = Math.max(0, Math.min(1, (clientX - actionData.volume.x) / actionData.volume.width));
7472
7473 target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin / 100)) + 'px)';
7474
7475 // determine logical volume, including background margin
7476 pixelMargin = ((backgroundMargin / 100) * actionData.volume.width);
7477
7478 volume = Math.max(0, Math.min(1, ((clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin * 2)))) * 100;
7479
7480 // set volume
7481 if (soundObject) {
7482 soundObject.setVolume(volume);
7483 }
7484
7485 defaultVolume = volume;
7486
7487 return utils.events.preventDefault(e);
7488
7489 },
7490
7491 releaseVolume: function(/* e */) {
7492
7493 utils.events.remove(document, 'mousemove', actions.adjustVolume);
7494 utils.events.remove(document, 'touchmove', actions.adjustVolume);
7495 utils.events.remove(document, 'mouseup', actions.releaseVolume);
7496 utils.events.remove(document, 'touchend', actions.releaseVolume);
7497
7498 },
7499
7500 setVolume: function(volume) {
7501
7502 // set volume (0-100) and update volume slider UI.
7503
7504 var backgroundSize,
7505 backgroundMargin,
7506 backgroundOffset,
7507 target,
7508 from,
7509 to;
7510
7511 if (volume === undefined || isNaN(volume)) {
7512 return;
7513 }
7514
7515 if (dom.volume) {
7516
7517 target = dom.volume;
7518
7519 // based on getStyle() result
7520 backgroundSize = actionData.volume.backgroundSize;
7521
7522 // figure out spacing around background image based on background size, eg. 60% background size.
7523 // 60% wide means 20% margin on each side.
7524 backgroundMargin = (100 - backgroundSize) / 2;
7525
7526 // margin as pixel value relative to width
7527 backgroundOffset = actionData.volume.width * (backgroundMargin / 100);
7528
7529 from = backgroundOffset;
7530 to = from + ((actionData.volume.width - (backgroundOffset * 2)) * (volume / 100));
7531
7532 target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)';
7533
7534 }
7535
7536 // apply volume to sound, as applicable
7537 if (soundObject) {
7538 soundObject.setVolume(volume);
7539 }
7540
7541 defaultVolume = volume;
7542
7543 }
7544
7545 };
7546
7547 init();
7548
7549 // TODO: mixin actions -> exports
7550
7551 exports = {
7552 // Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference.
7553 on: null,
7554 actions: actions,
7555 dom: dom,
7556 playlistController: playlistController
7557 };
7558
7559 return exports;
7560
7561 };
7562
7563 // barebones utilities for logic, CSS, DOM, events etc.
7564
7565 utils = {
7566
7567 array: (function() {
7568
7569 function compare(property) {
7570
7571 var result;
7572
7573 return function(a, b) {
7574
7575 if (a[property] < b[property]) {
7576 result = -1;
7577 } else if (a[property] > b[property]) {
7578 result = 1;
7579 } else {
7580 result = 0;
7581 }
7582 return result;
7583 };
7584
7585 }
7586
7587 function shuffle(array) {
7588
7589 // Fisher-Yates shuffle algo
7590
7591 var i, j, temp;
7592
7593 for (i = array.length - 1; i > 0; i--) {
7594 j = Math.floor(Math.random() * (i + 1));
7595 temp = array[i];
7596 array[i] = array[j];
7597 array[j] = temp;
7598 }
7599
7600 return array;
7601
7602 }
7603
7604 return {
7605 compare: compare,
7606 shuffle: shuffle
7607 };
7608
7609 }()),
7610
7611 css: (function() {
7612
7613 function hasClass(o, cStr) {
7614
7615 return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false);
7616
7617 }
7618
7619 function addClass(o, cStr) {
7620
7621 if (!o || !cStr || hasClass(o, cStr)) {
7622 return; // safety net
7623 }
7624 o.className = (o.className ? o.className + ' ' : '') + cStr;
7625
7626 }
7627
7628 function removeClass(o, cStr) {
7629
7630 if (!o || !cStr || !hasClass(o, cStr)) {
7631 return;
7632 }
7633 o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), '');
7634
7635 }
7636
7637 function swapClass(o, cStr1, cStr2) {
7638
7639 var tmpClass = {
7640 className: o.className
7641 };
7642
7643 removeClass(tmpClass, cStr1);
7644 addClass(tmpClass, cStr2);
7645
7646 o.className = tmpClass.className;
7647
7648 }
7649
7650 function toggleClass(o, cStr) {
7651
7652 var found,
7653 method;
7654
7655 found = hasClass(o, cStr);
7656
7657 method = (found ? removeClass : addClass);
7658
7659 method(o, cStr);
7660
7661 // indicate the new state...
7662 return !found;
7663
7664 }
7665
7666 return {
7667 has: hasClass,
7668 add: addClass,
7669 remove: removeClass,
7670 swap: swapClass,
7671 toggle: toggleClass
7672 };
7673
7674 }()),
7675
7676 dom: (function() {
7677
7678 function getAll(param1, param2) {
7679
7680 var node,
7681 selector,
7682 results;
7683
7684 if (arguments.length === 1) {
7685
7686 // .selector case
7687 node = document.documentElement;
7688 // first param is actually the selector
7689 selector = param1;
7690
7691 } else {
7692
7693 // node, .selector
7694 node = param1;
7695 selector = param2;
7696
7697 }
7698
7699 // sorry, IE 7 users; IE 8+ required.
7700 if (node && node.querySelectorAll) {
7701
7702 results = node.querySelectorAll(selector);
7703
7704 }
7705
7706 return results;
7707
7708 }
7709
7710 function get(/* parentNode, selector */) {
7711
7712 var results = getAll.apply(this, arguments);
7713
7714 // hackish: if an array, return the last item.
7715 if (results && results.length) {
7716 return results[results.length - 1];
7717 }
7718
7719 // handle "not found" case
7720 return results && results.length === 0 ? null : results;
7721
7722 }
7723
7724 function ancestor(nodeName, element, checkCurrent) {
7725
7726 if (!element || !nodeName) {
7727 return element;
7728 }
7729
7730 nodeName = nodeName.toUpperCase();
7731
7732 // return if current node matches.
7733 if (checkCurrent && element && element.nodeName === nodeName) {
7734 return element;
7735 }
7736
7737 while (element && element.nodeName !== nodeName && element.parentNode) {
7738 element = element.parentNode;
7739 }
7740
7741 return (element && element.nodeName === nodeName ? element : null);
7742
7743 }
7744
7745 return {
7746 ancestor: ancestor,
7747 get: get,
7748 getAll: getAll
7749 };
7750
7751 }()),
7752
7753 position: (function() {
7754
7755 function getOffX(o) {
7756
7757 // http://www.xs4all.nl/~ppk/js/findpos.html
7758 var curleft = 0;
7759
7760 if (o.offsetParent) {
7761
7762 while (o.offsetParent) {
7763
7764 curleft += o.offsetLeft;
7765
7766 o = o.offsetParent;
7767
7768 }
7769
7770 } else if (o.x) {
7771
7772 curleft += o.x;
7773
7774 }
7775
7776 return curleft;
7777
7778 }
7779
7780 function getOffY(o) {
7781
7782 // http://www.xs4all.nl/~ppk/js/findpos.html
7783 var curtop = 0;
7784
7785 if (o.offsetParent) {
7786
7787 while (o.offsetParent) {
7788
7789 curtop += o.offsetTop;
7790
7791 o = o.offsetParent;
7792
7793 }
7794
7795 } else if (o.y) {
7796
7797 curtop += o.y;
7798
7799 }
7800
7801 return curtop;
7802
7803 }
7804
7805 return {
7806 getOffX: getOffX,
7807 getOffY: getOffY
7808 };
7809
7810 }()),
7811
7812 style: (function() {
7813
7814 function get(node, styleProp) {
7815
7816 // http://www.quirksmode.org/dom/getstyles.html
7817 var value;
7818
7819 if (node.currentStyle) {
7820
7821 value = node.currentStyle[styleProp];
7822
7823 } else if (window.getComputedStyle) {
7824
7825 value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp);
7826
7827 }
7828
7829 return value;
7830
7831 }
7832
7833 return {
7834 get: get
7835 };
7836
7837 }()),
7838
7839 events: (function() {
7840
7841 var add, remove, preventDefault, getClientX;
7842
7843 add = function(o, evtName, evtHandler) {
7844 // return an object with a convenient detach method.
7845 var eventObject = {
7846 detach: function() {
7847 return remove(o, evtName, evtHandler);
7848 }
7849 };
7850 if (window.addEventListener) {
7851 o.addEventListener(evtName, evtHandler, false);
7852 } else {
7853 o.attachEvent('on' + evtName, evtHandler);
7854 }
7855 return eventObject;
7856 };
7857
7858 remove = (window.removeEventListener !== undefined ? function(o, evtName, evtHandler) {
7859 return o.removeEventListener(evtName, evtHandler, false);
7860 } : function(o, evtName, evtHandler) {
7861 return o.detachEvent('on' + evtName, evtHandler);
7862 });
7863
7864 preventDefault = function(e) {
7865 if (e.preventDefault) {
7866 e.preventDefault();
7867 } else {
7868 e.returnValue = false;
7869 e.cancelBubble = true;
7870 }
7871 return false;
7872 };
7873
7874 getClientX = function(e) {
7875 // normalize between desktop (mouse) and touch (mobile/tablet/?) events.
7876 // note pageX for touch, which normalizes zoom/scroll/pan vs. clientX.
7877 return (e && (e.clientX || (e.touches && e.touches[0] && e.touches[0].pageX)));
7878 };
7879
7880 return {
7881 add: add,
7882 preventDefault: preventDefault,
7883 remove: remove,
7884 getClientX: getClientX
7885 };
7886
7887 }()),
7888
7889 features: (function() {
7890
7891 var getAnimationFrame,
7892 localAnimationFrame,
7893 localFeatures,
7894 prop,
7895 styles,
7896 testDiv,
7897 transform;
7898
7899 testDiv = document.createElement('div');
7900
7901 /**
7902 * hat tip: paul irish
7903 * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
7904 * https://gist.github.com/838785
7905 */
7906
7907 localAnimationFrame = (window.requestAnimationFrame
7908 || window.webkitRequestAnimationFrame
7909 || window.mozRequestAnimationFrame
7910 || window.oRequestAnimationFrame
7911 || window.msRequestAnimationFrame
7912 || null);
7913
7914 // apply to window, avoid "illegal invocation" errors in Chrome
7915 getAnimationFrame = localAnimationFrame ? function() {
7916 return localAnimationFrame.apply(window, arguments);
7917 } : null;
7918
7919 function has(propName) {
7920
7921 // test for feature support
7922 return (testDiv.style[propName] !== undefined ? propName : null);
7923
7924 }
7925
7926 // note local scope.
7927 localFeatures = {
7928
7929 transform: {
7930 ie: has('-ms-transform'),
7931 moz: has('MozTransform'),
7932 opera: has('OTransform'),
7933 webkit: has('webkitTransform'),
7934 w3: has('transform'),
7935 prop: null // the normalized property value
7936 },
7937
7938 rotate: {
7939 has3D: false,
7940 prop: null
7941 },
7942
7943 getAnimationFrame: getAnimationFrame
7944
7945 };
7946
7947 localFeatures.transform.prop = (
7948 localFeatures.transform.w3 ||
7949 localFeatures.transform.moz ||
7950 localFeatures.transform.webkit ||
7951 localFeatures.transform.ie ||
7952 localFeatures.transform.opera
7953 );
7954
7955 function attempt(style) {
7956
7957 try {
7958 testDiv.style[transform] = style;
7959 } catch(e) {
7960 // that *definitely* didn't work.
7961 return false;
7962 }
7963 // if we can read back the style, it should be cool.
7964 return !!testDiv.style[transform];
7965
7966 }
7967
7968 if (localFeatures.transform.prop) {
7969
7970 // try to derive the rotate/3D support.
7971 transform = localFeatures.transform.prop;
7972 styles = {
7973 css_2d: 'rotate(0deg)',
7974 css_3d: 'rotate3d(0,0,0,0deg)'
7975 };
7976
7977 if (attempt(styles.css_3d)) {
7978 localFeatures.rotate.has3D = true;
7979 prop = 'rotate3d';
7980 } else if (attempt(styles.css_2d)) {
7981 prop = 'rotate';
7982 }
7983
7984 localFeatures.rotate.prop = prop;
7985
7986 }
7987
7988 testDiv = null;
7989
7990 return localFeatures;
7991
7992 }())
7993
7994 };
7995
7996 // ---
7997
7998 // expose to global
7999 window.sm2BarPlayers = players;
8000 window.sm2BarPlayerOptions = playerOptions;
8001 window.SM2BarPlayer = Player;
8002
8003}(window));</script>
8004
8005
8006
8007<style>
8008/**
8009 * SoundManager 2: "Bar UI" player - CSS
8010 * Copyright (c) 2014, Scott Schiller. All rights reserved.
8011 * http://www.schillmania.com/projects/soundmanager2/
8012 * Code provided under BSD license.
8013 * http://schillmania.com/projects/soundmanager2/license.txt
8014 */
8015
8016.sm2-bar-ui {
8017 position: relative;
8018 display: inline-block;
8019 width: 100%;
8020 font-family: helvetica, arial, verdana, sans-serif;
8021 font-weight: normal;
8022 /* prevent background border bleed */
8023 -webkit-background-clip: padding-box;
8024 background-clip: padding-box;
8025 /* because indeed, fonts do look pretty "fat" otherwise in this case. */
8026 -moz-osx-font-smoothing: grayscale;
8027 -webkit-font-smoothing: antialiased;
8028 /* general font niceness? */
8029 font-smoothing: antialiased;
8030 text-rendering: optimizeLegibility;
8031 min-width: 20em;
8032 max-width: 30em;
8033 /* take out overflow if you want an absolutely-positioned playlist dropdown. */
8034 border-radius: 2px;
8035 overflow: hidden;
8036 /* just for fun (animate normal / full-width) */
8037 transition: max-width 0.2s ease-in-out;
8038}
8039
8040.sm2-bar-ui .sm2-playlist li {
8041 text-align: center;
8042 margin-top: -2px;
8043 font-size: 95%;
8044 line-height: 1em;
8045}
8046
8047.sm2-bar-ui.compact {
8048 min-width: 1em;
8049 max-width: 15em;
8050}
8051
8052.sm2-bar-ui ul {
8053 line-height: 1em;
8054}
8055
8056/* want things left-aligned? */
8057.sm2-bar-ui.left .sm2-playlist li {
8058 text-align: left;
8059}
8060
8061.sm2-bar-ui .sm2-playlist li .load-error {
8062 cursor: help;
8063}
8064
8065.sm2-bar-ui.full-width {
8066 max-width: 100%;
8067 z-index: 5;
8068}
8069
8070.sm2-bar-ui.fixed {
8071 position: fixed;
8072 top: auto;
8073 bottom: 0px;
8074 left: 0px;
8075 border-radius: 0px;
8076 /* so the absolutely-positioned playlist can show... */
8077 overflow: visible;
8078 /* and this should probably have a high z-index. tweak as needed. */
8079 z-index: 999;
8080}
8081
8082.sm2-bar-ui.fixed .bd,
8083.sm2-bar-ui.bottom .bd {
8084 /* display: table; */
8085 border-radius: 0px;
8086 border-bottom: none;
8087}
8088
8089.sm2-bar-ui.bottom {
8090 /* absolute bottom-aligned UI */
8091 top: auto;
8092 bottom: 0px;
8093 left: 0px;
8094 border-radius: 0px;
8095 /* so the absolutely-positioned playlist can show... */
8096 overflow: visible;
8097}
8098
8099.sm2-bar-ui.playlist-open .bd {
8100 border-bottom-left-radius: 0px;
8101 border-bottom-right-radius: 0px;
8102 border-bottom-color: transparent;
8103}
8104
8105.sm2-bar-ui .bd,
8106.sm2-bar-ui .sm2-extra-controls {
8107 position: relative;
8108 background-color: #24272c;
8109 /*
8110 transition: background 0.2s ease-in-out;
8111 */
8112}
8113
8114.sm2-bar-ui .sm2-inline-gradient {
8115 /* gradient */
8116 position: absolute;
8117 left: 0px;
8118 top: 0px;
8119 width: 100%;
8120 height: 100%;
8121 background-image: linear-gradient(to bottom, rgba(255,255,255,0.125) 5%, rgba(255,255,255,0.125) 45%, rgba(255,255,255,0.15) 50%, rgba(0,0,0,0.1) 51%, rgba(0,0,0,0.1) 95%); /* W3C */
8122}
8123
8124.sm2-bar-ui.flat .sm2-inline-gradient {
8125 background-image: none;
8126}
8127
8128.sm2-bar-ui.flat .sm2-box-shadow {
8129 display: none;
8130 box-shadow: none;
8131}
8132
8133.sm2-bar-ui.no-volume .sm2-volume {
8134 /* mobile devices (android + iOS) ignore attempts to set volume. */
8135 display: none;
8136}
8137
8138.sm2-bar-ui.textured .sm2-inline-texture {
8139 position: absolute;
8140 top: 0px;
8141 left: 0px;
8142 width: 100%;
8143 height: 100%;
8144 /* for example */
8145 /* background-image: url(../image/wood_pattern_dark.png); */
8146 /* additional opacity effects can be applied here. */
8147 opacity: 0.75;
8148
8149}
8150
8151.sm2-bar-ui.textured.dark-text .sm2-inline-texture {
8152 /* dark text + textured case: use light wood background (for example.) */
8153 /* background-image: url(../image/patterns/wood_pattern.png); */
8154}
8155
8156.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper {
8157 /* dark text + textured case: ditch 10% dark on playlist body. */
8158 background-color: transparent;
8159}
8160
8161.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper ul li:hover a,
8162.sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper ul li.selected a {
8163 /* dark + textured case: dark highlights */
8164 background-color: rgba(0,0,0,0.1);
8165 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/black-10.png);
8166 /* modern browsers don't neeed the image */
8167 background-image: none, none;
8168}
8169
8170.sm2-bar-ui .bd {
8171 display: table;
8172 border-bottom: none;
8173}
8174
8175.sm2-bar-ui .sm2-playlist-wrapper {
8176 background-color: rgba(0,0,0,0.1);
8177}
8178
8179.sm2-bar-ui .sm2-extra-controls .bd {
8180 background-color: rgba(0,0,0,0.2);
8181}
8182
8183
8184.sm2-bar-ui.textured .sm2-extra-controls .bd {
8185 /* lighten extra color overlays */
8186 background-color: rgba(0,0,0,0.05);
8187}
8188
8189.sm2-bar-ui .sm2-extra-controls {
8190 background-color: transparent;
8191 border: none;
8192}
8193
8194.sm2-bar-ui .sm2-extra-controls .bd {
8195 /* override full-width table behaviour */
8196 display: block;
8197 border: none;
8198}
8199
8200.sm2-bar-ui .sm2-extra-controls .sm2-inline-element {
8201 display: inline-block;
8202}
8203
8204.sm2-bar-ui,
8205.sm2-bar-ui .bd a {
8206 color: #fff;
8207}
8208
8209.sm2-bar-ui.dark-text,
8210.sm2-bar-ui.dark-text .bd a {
8211 color: #000;
8212}
8213
8214.sm2-bar-ui.dark-text .sm2-inline-button {
8215 /* Warning/disclaimer: SVG might be fuzzy when inverted on Chrome, losing resolution on hi-DPI displays. */
8216 -webkit-filter: invert(1);
8217 /* SVG-based invert filter for Firefox */
8218 filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'invert\'><feColorMatrix in='SourceGraphic' type='matrix' values='-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0'/></filter></svg>#invert");
8219 /* IE 8 inverse filter, may only match pure black/white */
8220 /* filter: xray; */
8221 /* pending W3 standard */
8222 filter: invert(1);
8223 /* not you, IE < 10. */
8224 filter: none\9;
8225}
8226
8227.sm2-bar-ui .bd a {
8228 text-decoration: none;
8229}
8230
8231.sm2-bar-ui .bd .sm2-button-element:hover {
8232 background-color: rgba(0,0,0,0.1);
8233 background-image: url(../image/black-10.png);
8234 background-image: none, none;
8235}
8236
8237.sm2-bar-ui .bd .sm2-button-element:active {
8238 background-color: rgba(0,0,0,0.25);
8239 background-image: url(../image/black-25.png);
8240 background-image: none, none;
8241}
8242
8243.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active .sm2-inline-button,
8244.sm2-bar-ui .bd .active .sm2-inline-button/*,
8245.sm2-bar-ui.playlist-open .sm2-menu a */{
8246 -ms-transform: scale(0.9);
8247 -webkit-transform: scale(0.9);
8248 -webkit-transform-origin: 50% 50%;
8249 /* firefox doesn't scale quite right. */
8250 transform: scale(0.9);
8251 transform-origin: 50% 50%;
8252 /* firefox doesn't scale quite right. */
8253 -moz-transform: none;
8254}
8255
8256.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:hover,
8257.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active,
8258.sm2-bar-ui .bd .active {
8259 background-color: rgba(0,0,0,0.1);
8260 background-image: url(../image/black-10.png);
8261 background-image: none, none;
8262}
8263
8264.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active {
8265 /* box shadow is excessive on smaller elements. */
8266 box-shadow: none;
8267}
8268
8269.sm2-bar-ui {
8270 /* base font size */
8271 font-size: 15px;
8272 text-shadow: none;
8273}
8274
8275.sm2-bar-ui .sm2-inline-element {
8276 position: relative;
8277 display: inline-block;
8278 vertical-align: middle;
8279 padding: 0px;
8280 overflow: hidden;
8281}
8282
8283.sm2-bar-ui .sm2-inline-element,
8284.sm2-bar-ui .sm2-button-element .sm2-button-bd {
8285 position: relative;
8286 /**
8287 * .sm2-button-bd exists because of a Firefox bug from 2000
8288 * re: nested relative / absolute elements inside table cells.
8289 * https://bugzilla.mozilla.org/show_bug.cgi?id=63895
8290 */
8291}
8292
8293.sm2-bar-ui .sm2-inline-element,
8294.sm2-bar-ui .sm2-button-element .sm2-button-bd {
8295 /**
8296 * if you play with UI width/height, these are the important ones.
8297 * NOTE: match these values if you want square UI buttons.
8298 */
8299 min-width: 2.8em;
8300 min-height: 2.8em;
8301}
8302
8303.sm2-bar-ui .sm2-inline-button {
8304 position: absolute;
8305 top: 0px;
8306 left: 0px;
8307 width: 100%;
8308 height: 100%;
8309}
8310
8311.sm2-bar-ui .sm2-extra-controls .bd {
8312 /* don't double-layer. */
8313 background-image: none;
8314 background-color: rgba(0,0,0,0.15);
8315}
8316
8317.sm2-bar-ui .sm2-extra-controls .sm2-inline-element {
8318 width: 25px; /* bare minimum */
8319 min-height: 1.75em;
8320 min-width: 2.5em;
8321}
8322
8323.sm2-bar-ui .sm2-inline-status {
8324 line-height: 100%;
8325 /* how much to allow before truncating song artist / title with ellipsis */
8326 display: inline-block;
8327 min-width: 200px;
8328 max-width: 20em;
8329 /* a little more spacing */
8330 padding-left: 0.75em;
8331 padding-right: 0.75em;
8332}
8333
8334.sm2-bar-ui .sm2-inline-element {
8335 /* extra-small em scales up nicely, vs. 1px which gets fat */
8336 border-right: 0.075em dotted #666; /* legacy */
8337 border-right: 0.075em solid rgba(0,0,0,0.1);
8338}
8339
8340.sm2-bar-ui .sm2-inline-element.noborder {
8341 border-right: none;
8342}
8343
8344.sm2-bar-ui .sm2-inline-element.compact {
8345 min-width: 2em;
8346 padding: 0px 0.25em;
8347}
8348
8349.sm2-bar-ui .sm2-inline-element:first-of-type {
8350 border-top-left-radius: 3px;
8351 border-bottom-left-radius: 3px;
8352 overflow: hidden;
8353}
8354
8355.sm2-bar-ui .sm2-inline-element:last-of-type {
8356 border-right: none;
8357 border-top-right-radius: 3px;
8358 border-bottom-right-radius: 3px;
8359}
8360
8361.sm2-bar-ui .sm2-inline-status a:hover {
8362 background-color: transparent;
8363 text-decoration: underline;
8364}
8365
8366.sm2-inline-time,
8367.sm2-inline-duration {
8368 display: table-cell;
8369 width: 1%;
8370 font-size: 75%;
8371 line-height: 0.9em;
8372 min-width: 2em; /* if you have sounds > 10:00 in length, make this bigger. */
8373 vertical-align: middle;
8374}
8375
8376.sm2-bar-ui .sm2-playlist {
8377 position: relative;
8378 height: 1.45em;
8379}
8380
8381.sm2-bar-ui .sm2-playlist-target {
8382 /* initial render / empty case */
8383 position: relative;
8384 min-height: 1em;
8385}
8386
8387.sm2-bar-ui .sm2-playlist ul {
8388 position: absolute;
8389 left: 0px;
8390 top: 0px;
8391 width: 100%;
8392 list-style-type: none;
8393 overflow: hidden;
8394 white-space: nowrap;
8395 text-overflow: ellipsis;
8396}
8397
8398.sm2-bar-ui p,
8399.sm2-bar-ui .sm2-playlist ul,
8400.sm2-bar-ui .sm2-playlist ul li {
8401 margin: 0px;
8402 padding: 0px;
8403}
8404
8405.sm2-bar-ui .sm2-playlist ul li {
8406 position: relative;
8407}
8408
8409.sm2-bar-ui .sm2-playlist ul li,
8410.sm2-bar-ui .sm2-playlist ul li a {
8411 position: relative;
8412 display: block;
8413 /* prevent clipping of characters like "g" */
8414 height: 1.5em;
8415 white-space: nowrap;
8416 overflow: hidden;
8417 text-overflow: ellipsis;
8418 text-align: center;
8419}
8420
8421.sm2-row {
8422 position: relative;
8423 display: table-row;
8424}
8425
8426.sm2-progress-bd {
8427 /* spacing between progress track/ball and time (position) */
8428 padding: 0px 0.8em;
8429}
8430
8431.sm2-progress .sm2-progress-track,
8432.sm2-progress .sm2-progress-ball,
8433.sm2-progress .sm2-progress-bar {
8434 position: relative;
8435 width: 100%;
8436 height: 0.65em;
8437 border-radius: 0.65em;
8438}
8439
8440.sm2-progress .sm2-progress-bar {
8441 /* element which follows the progres "ball" as it moves */
8442 position: absolute;
8443 left: 0px;
8444 top: 0px;
8445 width: 0px;
8446 background-color: rgba(0,0,0,0.33);
8447 background-image: url(../image/black-33.png);
8448 background-image: none, none;
8449}
8450
8451.volume-shade,
8452.playing .sm2-progress .sm2-progress-track,
8453.paused .sm2-progress .sm2-progress-track {
8454 cursor: pointer;
8455}
8456
8457.playing .sm2-progress .sm2-progress-ball {
8458 cursor: -moz-grab;
8459 cursor: -webkit-grab;
8460 cursor: grab;
8461}
8462
8463.sm2-progress .sm2-progress-ball {
8464 position: absolute;
8465 top: 0px;
8466 left: 0px;
8467 width: 1em;
8468 height: 1em;
8469 margin: -0.2em 0px 0px -0.5em;
8470 width: 14px;
8471 height: 14px;
8472 margin: -2px 0px 0px -7px;
8473 width: 0.9333em;
8474 height: 0.9333em;
8475 margin: -0.175em 0px 0px -0.466em;
8476 background-color: #fff;
8477 padding: 0px;
8478/*
8479 z-index: 1;
8480*/
8481 transition: transform 0.15s ease-in-out;
8482}
8483
8484/*
8485.sm2-bar-ui.dark-text .sm2-progress .sm2-progress-ball {
8486 background-color: #000;
8487}
8488*/
8489
8490.sm2-progress .sm2-progress-track {
8491 background-color: rgba(0,0,0,0.4);
8492 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/black-33.png); /* legacy */
8493 background-image: none, none; /* modern browsers */
8494}
8495
8496/* scrollbar rules have to be separate, browsers not supporting this syntax will skip them when combined. */
8497.sm2-playlist-wrapper ul::-webkit-scrollbar-track {
8498 background-color: rgba(0,0,0,0.4);
8499}
8500
8501.playing.grabbing .sm2-progress .sm2-progress-track,
8502.playing.grabbing .sm2-progress .sm2-progress-ball {
8503 cursor: -moz-grabbing;
8504 cursor: -webkit-grabbing;
8505 cursor: grabbing;
8506}
8507
8508.sm2-bar-ui.grabbing .sm2-progress .sm2-progress-ball {
8509 -webkit-transform: scale(1.15);
8510 transform: scale(1.15);
8511}
8512
8513.sm2-inline-button {
8514 background-position: 50% 50%;
8515 background-repeat: no-repeat;
8516 /* hide inner text */
8517 line-height: 10em;
8518 /**
8519 * image-rendering seems to apply mostly to Firefox in this case. Use with caution.
8520 * https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#Browser_compatibility
8521 */
8522 image-rendering: -moz-crisp-edges;
8523 image-rendering: -webkit-optimize-contrast;
8524 image-rendering: crisp-edges;
8525 -ms-interpolation-mode: nearest-neighbor;
8526 -ms-interpolation-mode: bicubic;
8527}
8528
8529.sm2-icon-play-pause,
8530.sm2-icon-play-pause:hover,
8531.paused .sm2-icon-play-pause:hover {
8532 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/PNG/play.png);
8533 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/SVG/play.svg);
8534 background-size: 67.5%;
8535 background-position: 40% 53%;
8536}
8537
8538.playing .sm2-icon-play-pause {
8539 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/PNG/pause.png);
8540 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/SVG/pause.svg);
8541 background-size: 57.6%;
8542 background-position: 50% 53%;
8543}
8544
8545.sm2-volume-control {
8546 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/PNG/volume.png);
8547 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/SVG/volume.svg);
8548}
8549
8550.sm2-volume-control,
8551.sm2-volume-shade {
8552 background-position: 42% 50%;
8553 background-size: 56%;
8554}
8555
8556.volume-shade {
8557 filter: alpha(opacity=33); /* <= IE 8 */
8558 opacity: 0.33;
8559/* -webkit-filter: invert(1);*/
8560 background-image: url(../image/icomoon/entypo-25px-000000/PNG/volume.png);
8561 background-image: none, url(../image/icomoon/entypo-25px-000000/SVG/volume.svg);
8562}
8563
8564.sm2-icon-menu {
8565 background-image: url(../image/icomoon/entypo-25px-ffffff/PNG/list2.png);
8566 background-image: none, url(../image/icomoon/entypo-25px-ffffff/SVG/list2.svg);
8567 background-size: 58%;
8568 background-position: 54% 51%;
8569}
8570
8571.sm2-icon-previous {
8572 background-image: url(../image/icomoon/entypo-25px-ffffff/PNG/first.png);
8573 background-image: none, url(../image/icomoon/entypo-25px-ffffff/SVG/first.svg);
8574}
8575
8576.sm2-icon-next {
8577 background-image: url(../image/icomoon/entypo-25px-ffffff/PNG/last.png);
8578 background-image: none, url(../image/icomoon/entypo-25px-ffffff/SVG/last.svg);
8579}
8580
8581.sm2-icon-previous,
8582.sm2-icon-next {
8583 background-size: 49.5%;
8584 background-position: 50% 50%;
8585}
8586
8587
8588.sm2-extra-controls .sm2-icon-previous,
8589.sm2-extra-controls .sm2-icon-next {
8590 backgound-size: 53%;
8591}
8592
8593.sm2-icon-shuffle {
8594 background-image: url(../image/icomoon/entypo-25px-ffffff/PNG/shuffle.png);
8595 background-image: none, url(../image/icomoon/entypo-25px-ffffff/SVG/shuffle.svg);
8596 background-size: 45%;
8597 background-position: 50% 50%;
8598}
8599
8600.sm2-icon-repeat {
8601 background-image: url(../image/icomoon/entypo-25px-ffffff/PNG/loop.png);
8602 background-image: none, url(../image/icomoon/entypo-25px-ffffff/SVG/loop.svg);
8603 background-position: 50% 43%;
8604 background-size: 54%;
8605}
8606
8607.sm2-extra-controls .sm2-icon-repeat {
8608 background-position: 50% 45%;
8609}
8610
8611.sm2-playlist-wrapper ul li .sm2-row {
8612 display: table;
8613 width: 100%;
8614}
8615
8616.sm2-playlist-wrapper ul li .sm2-col {
8617 display: table-cell;
8618 vertical-align: top;
8619 /* by default, collapse. */
8620 width: 0%;
8621}
8622
8623.sm2-playlist-wrapper ul li .sm2-col.sm2-wide {
8624 /* take 100% width. */
8625 width: 100%;
8626}
8627
8628.sm2-playlist-wrapper ul li .sm2-icon {
8629 display: inline-block;
8630 overflow: hidden;
8631 width: 2em;
8632 color: transparent !important; /* hide text */
8633 white-space: nowrap; /* don't let text affect height */
8634 padding-left: 0px;
8635 padding-right: 0px;
8636 text-indent: 2em; /* IE 8, mostly */
8637}
8638
8639.sm2-playlist-wrapper ul li .sm2-icon,
8640.sm2-playlist-wrapper ul li:hover .sm2-icon,
8641.sm2-playlist-wrapper ul li.selected .sm2-icon {
8642 background-size: 55%;
8643 background-position: 50% 50%;
8644 background-repeat: no-repeat;
8645}
8646
8647.sm2-playlist-wrapper ul li .sm2-col {
8648 /* sibling table cells get borders. */
8649 border-right: 1px solid rgba(0,0,0,0.075);
8650}
8651
8652.sm2-playlist-wrapper ul li.selected .sm2-col {
8653 border-color: rgba(255,255,255,0.075);
8654}
8655
8656.sm2-playlist-wrapper ul li .sm2-col:last-of-type {
8657 border-right: none;
8658}
8659
8660.sm2-playlist-wrapper ul li .sm2-cart,
8661.sm2-playlist-wrapper ul li:hover .sm2-cart,
8662.sm2-playlist-wrapper ul li.selected .sm2-cart {
8663 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/PNG/cart.png);
8664 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/SVG/cart.svg);
8665 /* slight alignment tweak */
8666 background-position: 48% 50%;
8667}
8668
8669.sm2-playlist-wrapper ul li .sm2-music,
8670.sm2-playlist-wrapper ul li:hover .sm2-music,
8671.sm2-playlist-wrapper ul li.selected .sm2-music {
8672 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/PNG/music.png);
8673 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-ffffff/SVG/music.svg);
8674}
8675
8676.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-cart,
8677.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-cart,
8678.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-cart {
8679 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-000000/PNG/cart.png);
8680 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-000000/SVG/cart.svg);
8681}
8682
8683.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-music,
8684.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-music,
8685.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-music {
8686 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-000000/PNG/music.png);
8687 background-image: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/entypo-25px-000000/SVG/music.svg);
8688}
8689
8690
8691.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-col {
8692 border-left-color: rgba(0,0,0,0.15);
8693}
8694
8695.sm2-playlist-wrapper ul li .sm2-icon:hover {
8696 background-color: rgba(0,0,0,0.33);
8697}
8698
8699.sm2-bar-ui .sm2-playlist-wrapper ul li .sm2-icon:hover {
8700 background-color: rgba(0,0,0,0.45);
8701}
8702
8703.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-icon:hover {
8704 background-color: rgba(255,255,255,0.25);
8705 border-color: rgba(0,0,0,0.125);
8706}
8707
8708.sm2-progress-ball .icon-overlay {
8709 position: absolute;
8710 width: 100%;
8711 height: 100%;
8712 top: 0px;
8713 left: 0px;
8714 background: none, url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/icomoon/free-25px-000000/SVG/spinner.svg);
8715 background-size: 72%;
8716 background-position: 50%;
8717 background-repeat: no-repeat;
8718 display: none;
8719}
8720
8721.playing.buffering .sm2-progress-ball .icon-overlay {
8722 display: block;
8723 -webkit-animation: spin 0.6s linear infinite;
8724 animation: spin 0.6s linear infinite;
8725}
8726
8727@-webkit-keyframes spin {
8728 0% {
8729 -webkit-transform: rotate(0deg);
8730 }
8731 100% {
8732 -webkit-transform: rotate(360deg);
8733 }
8734}
8735
8736@-moz-keyframes spin {
8737 0% {
8738 transform: rotate(0deg);
8739 }
8740 100% {
8741 transform: rotate(360deg);
8742 }
8743}
8744
8745@keyframes spin {
8746 0% {
8747 transform: rotate(0deg);
8748 }
8749 100% {
8750 transform: rotate(360deg);
8751 }
8752}
8753
8754.sm2-element ul {
8755 font-size: 95%;
8756 list-style-type: none;
8757}
8758
8759.sm2-element ul,
8760.sm2-element ul li {
8761 margin: 0px;
8762 padding: 0px;
8763}
8764
8765.bd.sm2-playlist-drawer {
8766 /* optional: absolute positioning */
8767 /* position: absolute; */
8768 z-index: 3;
8769 border-radius: 0px;
8770 width: 100%;
8771 height: 0px;
8772 border: none;
8773 background-image: none;
8774 display: block;
8775 overflow: hidden;
8776 transition: height 0.2s ease-in-out;
8777}
8778
8779.sm2-bar-ui.fixed .bd.sm2-playlist-drawer,
8780.sm2-bar-ui.bottom .bd.sm2-playlist-drawer {
8781 position: absolute;
8782}
8783
8784.sm2-bar-ui.fixed .sm2-playlist-wrapper,
8785.sm2-bar-ui.bottom .sm2-playlist-wrapper {
8786 padding-bottom: 0px;
8787}
8788
8789.sm2-bar-ui.fixed .bd.sm2-playlist-drawer,
8790.sm2-bar-ui.bottom .bd.sm2-playlist-drawer {
8791 /* show playlist on top */
8792 bottom: 2.8em;
8793}
8794
8795.sm2-bar-ui .bd.sm2-playlist-drawer {
8796 opacity: 0.5;
8797 /* redraw fix for Chrome, background color doesn't always draw when playlist drawer open. */
8798 transform: translateZ(0);
8799}
8800
8801/* experimental, may not perform well. */
8802/*
8803.sm2-bar-ui .bd.sm2-playlist-drawer a {
8804 -webkit-filter: blur(5px);
8805}
8806*/
8807
8808.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer {
8809 height: auto;
8810 opacity: 1;
8811}
8812
8813.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer a {
8814 -webkit-filter: none; /* blur(0px) was still blurred on retina displays, as of 07/2014 */
8815}
8816
8817.sm2-bar-ui.fixed.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper,
8818.sm2-bar-ui.bottom.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper {
8819 /* extra padding when open */
8820 padding-bottom: 0.5em;
8821 box-shadow: none;
8822}
8823
8824.sm2-bar-ui .bd.sm2-playlist-drawer {
8825 transition: all 0.2s ease-in-out;
8826 transition-property: transform, height, opacity, background-color, -webkit-filter;
8827}
8828
8829.sm2-bar-ui .bd.sm2-playlist-drawer a {
8830 transition: -webkit-filter 0.2s ease-in-out;
8831}
8832
8833.sm2-bar-ui .bd.sm2-playlist-drawer .sm2-inline-texture {
8834 /* negative offset for height of top bar, so background is seamless. */
8835 background-position: 0px -2.8em;
8836}
8837
8838.sm2-box-shadow {
8839 position: absolute;
8840 left: 0px;
8841 top: 0px;
8842 width: 100%;
8843 height: 100%;
8844 box-shadow: inset 0px 1px 6px rgba(0,0,0,0.15);
8845}
8846
8847.sm2-playlist-wrapper {
8848 position: relative;
8849 padding: 0.5em 0.5em 0.5em 0.25em;
8850 background-image: none, none;
8851}
8852
8853.sm2-playlist-wrapper ul {
8854 max-height: 9.25em;
8855 overflow: auto;
8856}
8857
8858.sm2-playlist-wrapper ul li {
8859 border-bottom: 1px solid rgba(0,0,0,0.05);
8860}
8861
8862.sm2-playlist-wrapper ul li:nth-child(odd) {
8863 background-color: rgba(255,255,255,0.03);
8864}
8865
8866.sm2-playlist-wrapper ul li a {
8867 display: block;
8868 padding: 0.5em 0.25em 0.5em 0.75em;
8869 margin-right: 0px;
8870 font-size: 90%;
8871 vertical-align: middle;
8872}
8873
8874.sm2-playlist-wrapper ul li a.sm2-exclude {
8875 display: inline-block;
8876}
8877
8878.sm2-playlist-wrapper ul li a.sm2-exclude .label {
8879 font-size: 95%;
8880 line-height: 1em;
8881 margin-left: 0px;
8882 padding: 2px 4px;
8883}
8884
8885.sm2-playlist-wrapper ul li:hover a {
8886 background-color: rgba(0,0,0,0.20);
8887 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/black-20.png);
8888 background-image: none, none;
8889}
8890
8891.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover a {
8892 background-color: rgba(255,255,255,0.1);
8893 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/black-10.png);
8894 background-image: none, none;
8895}
8896
8897.sm2-playlist-wrapper ul li.selected a {
8898 background-color: rgba(0,0,0,0.25);
8899 background-image: url(http://www.schillmania.com/projects/soundmanager2/demo/bar-ui/image/black-20.png);
8900 background-image: none, none;
8901}
8902
8903.sm2-bar-ui.dark-text ul li.selected a {
8904 background-color: rgba(255,255,255,0.1);
8905 background-image: url(../image/black-10.png);
8906 background-image: none, none;
8907}
8908
8909.sm2-bar-ui .disabled {
8910 filter: alpha(opacity=33); /* <= IE 8 */
8911 opacity: 0.33;
8912}
8913
8914.sm2-bar-ui .bd .sm2-button-element.disabled:hover {
8915 background-color: transparent;
8916}
8917
8918.sm2-bar-ui .active,
8919/*.sm2-bar-ui.playlist-open .sm2-menu,*/
8920.sm2-bar-ui.playlist-open .sm2-menu:hover {
8921 /* depressed / "on" state */
8922 box-shadow: inset 0px 0px 2px rgba(0,0,0,0.1);
8923 background-image: none;
8924}
8925
8926.firefox-fix {
8927 /**
8928 * This exists because of a Firefox bug from 2000
8929 * re: nested relative / absolute elements inside table cells.
8930 * https://bugzilla.mozilla.org/show_bug.cgi?id=63895
8931 */
8932 position: relative;
8933 display: inline-block;
8934 width: 100%;
8935 height: 100%;
8936}
8937
8938/* some custom scrollbar trickery, where supported */
8939
8940.sm2-playlist-wrapper ul::-webkit-scrollbar {
8941 width: 10px;
8942}
8943
8944.sm2-playlist-wrapper ul::-webkit-scrollbar-track {
8945 background: rgba(0,0,0,0.33);
8946 border-radius: 10px;
8947}
8948
8949.sm2-playlist-wrapper ul::-webkit-scrollbar-thumb {
8950 border-radius: 10px;
8951 background: #fff;
8952}
8953
8954.sm2-extra-controls {
8955 font-size: 0px;
8956 text-align: center;
8957}
8958
8959.sm2-bar-ui .label {
8960 position: relative;
8961 display: inline-block;
8962 font-size: 0.7em;
8963 margin-left: 0.25em;
8964 vertical-align: top;
8965 background-color: rgba(0,0,0,0.25);
8966 border-radius: 3px;
8967 padding: 0px 3px;
8968 box-sizing: padding-box;
8969}
8970
8971.sm2-bar-ui.dark-text .label {
8972 background-color: rgba(0,0,0,0.1);
8973 background-image: url(../image/black-10.png);
8974 background-image: none, none;
8975}
8976
8977.sm2-bar-ui .sm2-playlist-drawer .label {
8978 font-size: 0.8em;
8979 padding: 0px 3px;
8980}
8981
8982/* --- full width stuff --- */
8983
8984.sm2-bar-ui .sm2-inline-element {
8985 display: table-cell;
8986}
8987
8988.sm2-bar-ui .sm2-inline-element {
8989 /* collapse */
8990 width: 1%;
8991}
8992
8993.sm2-bar-ui .sm2-inline-status {
8994 /* full width */
8995 width: 100%;
8996 min-width: 100%;
8997 max-width: 100%;
8998}
8999
9000.sm2-bar-ui > .bd {
9001 width: 100%;
9002}
9003
9004.sm2-bar-ui .sm2-playlist-drawer {
9005 /* re-hide playlist */
9006 display: block;
9007 overflow: hidden;
9008}
9009
9010</style>