/* SoundManager 2 Demo: "Page as playlist" ---------------------------------------------- http://schillmania.com/projects/soundmanager2/ An example of a Muxtape.com-style UI, where an unordered list of MP3 links becomes a playlist Flash 9 "MovieStar" edition supports MPEG4 audio as well. Requires SoundManager 2 Javascript API. */ function PagePlayer(oConfigOverride) { var self = this; var pl = this; var sm = soundManager; // soundManager instance // sniffing for favicon stuff/IE workarounds var uA = navigator.userAgent; var isIE = uA.match(/msie/i); var isOpera = uA.match(/opera/i); var isFirefox = uA.match(/firefox/i); var isTouchDevice = (uA.match(/ipad|iphone/i)); this.config = { flashVersion: 8, // version of Flash to tell SoundManager to use - either 8 or 9. Flash 9 required for peak / spectrum data. usePeakData: false, // [Flash 9 only]: show peak data useWaveformData: false, // [Flash 9 only]: enable sound spectrum (raw waveform data) - WARNING: CPU-INTENSIVE: may set CPUs on fire. useEQData: false, // [Flash 9 only]: enable sound EQ (frequency spectrum data) - WARNING: Also CPU-intensive. fillGraph: false, // [Flash 9 only]: draw full lines instead of only top (peak) spectrum points allowRightClick:true, // let users right-click MP3 links ("save as...", etc.) or discourage (can't prevent.) useThrottling: true, // try to rate-limit potentially-expensive calls (eg. dragging position around) autoStart: false, // begin playing first sound when page loads playNext: true, // stop after one sound, or play through list until end updatePageTitle: true, // change the page title while playing sounds emptyTime: '-:--', // null/undefined timer values (before data is available) useFavIcon: false // try to show peakData in address bar (Firefox + Opera) - may be too CPU heavy } sm.debugMode = (window.location.href.toString().match(/debug=1/i)?true:false); // enable with #debug=1 for example this._mergeObjects = function(oMain,oAdd) { // non-destructive merge var o1 = {}; // clone o1 for (var i in oMain) { o1[i] = oMain[i]; } var o2 = (typeof oAdd == 'undefined'?{}:oAdd); for (var o in o2) { if (typeof o1[o] == 'undefined') o1[o] = o2[o]; } return o1; } if (typeof oConfigOverride != 'undefined' && oConfigOverride) { // allow overriding via arguments object this.config = this._mergeObjects(oConfigOverride,this.config); } this.css = { // CSS class names appended to link during various states sDefault: 'sm2_link', // default state sLoading: 'sm2_loading', sPlaying: 'sm2_playing', sPaused: 'sm2_paused' } // apply externally-defined override, if applicable this.cssBase = []; // optional features added to ul.playlist if (this.config.usePeakData) this.cssBase.push('use-peak'); if (this.config.useWaveformData || this.config.useEQData) this.cssBase.push('use-spectrum'); this.cssBase = this.cssBase.join(' '); // apply some items to SM2 sm.useFlashBlock = true; sm.flashVersion = this.config.flashVersion; if (sm.flashVersion >= 9) { sm.useMovieStar = this.config.useMovieStar; // enable playing FLV, MP4 etc. sm.defaultOptions.usePeakData = this.config.usePeakData; sm.defaultOptions.useWaveformData = this.config.useWaveformData; sm.defaultOptions.useEQData = this.config.useEQData; } this.links = []; this.sounds = []; this.soundsByObject = []; this.lastSound = null; this.soundCount = 0; this.strings = []; this.dragActive = false; this.dragExec = new Date(); this.dragTimer = null; this.pageTitle = document.title; this.lastWPExec = new Date(); this.lastWLExec = new Date(); this.vuMeterData = []; this.oControls = null; this.addEventHandler = function(o,evtName,evtHandler) { typeof(attachEvent)=='undefined'?o.addEventListener(evtName,evtHandler,false):o.attachEvent('on'+evtName,evtHandler); } this.removeEventHandler = function(o,evtName,evtHandler) { typeof(attachEvent)=='undefined'?o.removeEventListener(evtName,evtHandler,false):o.detachEvent('on'+evtName,evtHandler); } this.hasClass = function(o,cStr) { return (typeof(o.className)!='undefined'?new RegExp('(^|\\s)'+cStr+'(\\s|$)').test(o.className):false); } this.addClass = function(o,cStr) { if (!o || !cStr) return false; // safety net if (self.hasClass(o,cStr)) return false; o.className = (o.className?o.className+' ':'')+cStr; } this.removeClass = function(o,cStr) { if (!o || !cStr) return false; // safety net if (!self.hasClass(o,cStr)) return false; o.className = o.className.replace(new RegExp('( '+cStr+')|('+cStr+')','g'),''); } this.getElementsByClassName = function(className,tagNames,oParent) { var doc = (oParent?oParent:document); var matches = []; var i,j; var nodes = []; if (typeof(tagNames)!='undefined' && typeof(tagNames)!='string') { for (i=tagNames.length; i--;) { if (!nodes || !nodes[tagNames[i]]) { nodes[tagNames[i]] = doc.getElementsByTagName(tagNames[i]); } } } else if (tagNames) { nodes = doc.getElementsByTagName(tagNames); } else { nodes = doc.all||doc.getElementsByTagName('*'); } if (typeof(tagNames)!='string') { for (i=tagNames.length; i--;) { for (j=nodes[tagNames[i]].length; j--;) { if (self.hasClass(nodes[tagNames[i]][j],className)) { matches[matches.length] = nodes[tagNames[i]][j]; } } } } else { for (i=0; i30 || this.bytesLoaded === this.bytesTotal) { doWork.apply(this); self.lastWLExec = d; } } }, onload: function() { if (!this.loaded) { var oTemp = this._data.oLI.getElementsByTagName('a')[0]; var oString = oTemp.innerHTML; var oThis = this; oTemp.innerHTML = oString+' | Load failed, d\'oh! '+(sm.sandbox.noRemote?' Possible cause: Flash sandbox is denying remote URL access.':(sm.sandbox.noLocal?'Flash denying local filesystem access':'404?'))+''; setTimeout(function(){ oTemp.innerHTML = oString; // pl.events.finish.apply(oThis); // load next },5000); } else { if (this._data.metadata) { this._data.metadata.refresh(); } } }, whileplaying: function() { var d = null; if (pl.dragActive || !pl.config.useThrottling) { self.updateTime.apply(this); if (sm.flashVersion >= 9) { if (pl.config.usePeakData && this.instanceOptions.usePeakData) self.updatePeaks.apply(this); if (pl.config.useWaveformData && this.instanceOptions.useWaveformData || pl.config.useEQData && this.instanceOptions.useEQData) { self.updateGraph.apply(this); } } if (this._data.metadata) { d = new Date(); if (d && d-self.lastWPExec>500) { self.refreshMetadata(this); self.lastWPExec = d; } } this._data.oPosition.style.width = (((this.position/self.getDurationEstimate(this))*100)+'%'); } else { d = new Date(); if (d-self.lastWPExec>30) { self.updateTime.apply(this); if (sm.flashVersion >= 9) { if (pl.config.usePeakData && this.instanceOptions.usePeakData) { self.updatePeaks.apply(this); } if (pl.config.useWaveformData && this.instanceOptions.useWaveformData || pl.config.useEQData && this.instanceOptions.useEQData) { self.updateGraph.apply(this); } } if (this._data.metadata) self.refreshMetadata(this); this._data.oPosition.style.width = (((this.position/self.getDurationEstimate(this))*100)+'%'); self.lastWPExec = d; } } } } // events{} var _head = document.getElementsByTagName('head')[0]; this.setPageIcon = function(sDataURL) { if (!self.config.useFavIcon || !self.config.usePeakData || !sDataURL) { return false; } var link = document.getElementById('sm2-favicon'); if (link) { _head.removeChild(link); link = null; } if (!link) { link = document.createElement('link'); link.id = 'sm2-favicon'; link.rel = 'shortcut icon'; link.type = 'image/png'; link.href = sDataURL; document.getElementsByTagName('head')[0].appendChild(link); } } this.resetPageIcon = function() { if (!self.config.useFavIcon) { return false; } var link = document.getElementById('favicon'); if (link) { link.href = '/favicon.ico'; } } this.updatePeaks = function() { var o = this._data.oPeak; var oSpan = o.getElementsByTagName('span'); oSpan[0].style.marginTop = (13-(Math.floor(15*this.peakData.left))+'px'); oSpan[1].style.marginTop = (13-(Math.floor(15*this.peakData.right))+'px'); // highly experimental if (self.config.flashVersion > 8 && self.config.useFavIcon && self.config.usePeakData) { self.setPageIcon(self.vuMeterData[parseInt(16*this.peakData.left)][parseInt(16*this.peakData.right)]); } } this.updateGraph = function() { if ((!pl.config.useWaveformData && !pl.config.useEQData) || pl.config.flashVersion<9) return false; var sbC = this._data.oGraph.getElementsByTagName('div'); if (pl.config.useWaveformData) { // raw waveform var scale = 8; // Y axis (+/- this distance from 0) for (var i=255; i--;) { sbC[255-i].style.marginTop = (1+scale+Math.ceil(this.waveformData.left[i]*-scale))+'px'; } } else { // eq spectrum var offset = 9; for (var i=255; i--;) { sbC[255-i].style.marginTop = ((offset*2)-1+Math.ceil(this.eqData[i]*-offset))+'px'; } } } this.resetGraph = function() { if (!pl.config.useEQData || pl.config.flashVersion<9) return false; var sbC = this._data.oGraph.getElementsByTagName('div'); var scale = (!pl.config.useEQData?'9px':'17px'); var nHeight = (!pl.config.fillGraph?'1px':'32px'); for (var i=255; i--;) { sbC[255-i].style.marginTop = scale; // EQ scale sbC[255-i].style.height = nHeight; } } this.refreshMetadata = function(oSound) { // Display info as appropriate var index = null; var now = oSound.position; var metadata = oSound._data.metadata.data; for (var i=0, j=metadata.length; i= metadata[i].startTimeMS && now <= metadata[i].endTimeMS) { index = i; break; } } if (index != metadata.currentItem) { // update oSound._data.oLink.innerHTML = metadata.mainTitle+' '; self.setPageTitle(metadata[index].title+' | '+metadata.mainTitle); metadata.currentItem = index; } } this.updateTime = function() { var str = self.strings['timing'].replace('%s1',self.getTime(this.position,true)); str = str.replace('%s2',self.getTime(self.getDurationEstimate(this),true)); this._data.oTiming.innerHTML = str; } this.getTheDamnTarget = function(e) { return (e.target||(window.event?window.event.srcElement:null)); } this.withinStatusBar = function(o) { return (self.isChildOfClass(o,'controls')); } this.handleClick = function(e) { // a sound (or something) was clicked - determine what and handle appropriately if (e.button == 2) { if (!pl.config.allowRightClick) { pl.stopEvent(e); } return pl.config.allowRightClick; // ignore right-clicks } var o = self.getTheDamnTarget(e); if (!o) { return true; } if (self.dragActive) self.stopDrag(); // to be safe if (self.withinStatusBar(o)) { // self.handleStatusClick(e); return false; } if (o.nodeName.toLowerCase() != 'a') { o = self.getParentByNodeName(o,'a'); } if (!o) { // not a link return true; } var sURL = o.getAttribute('href'); if (!o.href || (!sm.canPlayLink(o) && !self.hasClass(o,'playable')) || self.hasClass(o,'exclude')) { // do nothing, don't return anything. } else { // we have something we're interested in. var soundURL = o.href; var thisSound = self.getSoundByObject(o); if (thisSound) { // sound already exists self.setPageTitle(thisSound._data.originalTitle); if (thisSound == self.lastSound) { // ..and was playing (or paused) and isn't in an error state if (thisSound.readyState != 2) { if (thisSound.playState != 1) { // not yet playing thisSound.play(); } else { thisSound.togglePause(); } } else { sm._writeDebug('Warning: sound failed to load (security restrictions, 404 or bad format)',2); } } else { // ..different sound if (self.lastSound) self.stopSound(self.lastSound); thisSound._data.oTimingBox.appendChild(document.getElementById('spectrum-container')); thisSound.togglePause(); // start playing current } } else { // create sound thisSound = sm.createSound({ id:'pagePlayerMP3Sound'+(self.soundCount++), url:decodeURI(soundURL), onplay:self.events.play, onstop:self.events.stop, onpause:self.events.pause, onresume:self.events.resume, onfinish:self.events.finish, whileloading:self.events.whileloading, whileplaying:self.events.whileplaying, onmetadata:self.events.metadata, onload:self.events.onload }); // append control template var oControls = self.oControls.cloneNode(true); o.parentNode.appendChild(oControls); o.parentNode.appendChild(document.getElementById('spectrum-container')); self.soundsByObject[o.rel] = thisSound; // tack on some custom data thisSound._data = { oLink: o, // DOM reference within SM2 object event handlers oLI: o.parentNode, oControls: self.getElementsByClassName('controls','div',o.parentNode)[0], oStatus: self.getElementsByClassName('statusbar','div',o.parentNode)[0], oLoading: self.getElementsByClassName('loading','div',o.parentNode)[0], oPosition: self.getElementsByClassName('position','div',o.parentNode)[0], oTimingBox: self.getElementsByClassName('timing','div',o.parentNode)[0], oTiming: self.getElementsByClassName('timing','div',o.parentNode)[0].getElementsByTagName('div')[0], oPeak: self.getElementsByClassName('peak','div',o.parentNode)[0], oGraph: self.getElementsByClassName('spectrum-box','div',o.parentNode)[0], nIndex: self.getSoundIndex(o), className: self.css.sPlaying, originalTitle: o.innerHTML, metadata: null }; thisSound._data.oTimingBox.appendChild(document.getElementById('spectrum-container')); // "Metadata" if (thisSound._data.oLI.getElementsByTagName('ul').length) { thisSound._data.metadata = new Metadata(thisSound); } // set initial timer stuff (before loading) var str = self.strings['timing'].replace('%s1',self.config.emptyTime); str = str.replace('%s2',self.config.emptyTime); thisSound._data.oTiming.innerHTML = str; self.sounds.push(thisSound); if (self.lastSound) self.stopSound(self.lastSound); self.resetGraph.apply(thisSound); thisSound.play(); } self.lastSound = thisSound; // reference for next call return self.stopEvent(e); } } this.handleMouseDown = function(e) { // a sound link was clicked if (isTouchDevice && e.touches) { e = e.touches[0]; } if (e.button == 2) { if (!pl.config.allowRightClick) pl.stopEvent(e); return (pl.config.allowRightClick); // ignore right-clicks } var o = self.getTheDamnTarget(e); if (!o) { return true; } if (!self.withinStatusBar(o)) return true; self.dragActive = true; self.lastSound.pause(); self.setPosition(e); if (!isTouchDevice) { self.addEventHandler(document,'mousemove',self.handleMouseMove); } else { self.addEventHandler(document,'touchmove',self.handleMouseMove); } self.addClass(self.lastSound._data.oControls,'dragging'); self.stopEvent(e); return false; } this.handleMouseMove = function(e) { if (isTouchDevice && e.touches) { e = e.touches[0]; } // set position accordingly if (self.dragActive) { if (self.config.useThrottling) { // be nice to CPU/externalInterface var d = new Date(); if (d-self.dragExec>20) { self.setPosition(e); } else { window.clearTimeout(self.dragTimer); self.dragTimer = window.setTimeout(function(){self.setPosition(e)},20); } self.dragExec = d; } else { // oh the hell with it self.setPosition(e); } } else { self.stopDrag(); } e.stopPropagation = true; return false; } this.stopDrag = function(e) { if (self.dragActive) { self.removeClass(self.lastSound._data.oControls,'dragging'); if (!isTouchDevice) { self.removeEventHandler(document,'mousemove',self.handleMouseMove); } else { self.removeEventHandler(document,'touchmove',self.handleMouseMove); } // self.removeEventHandler(document,'mouseup',self.stopDrag); if (!pl.hasClass(self.lastSound._data.oLI,self.css.sPaused)) { self.lastSound.resume(); } self.dragActive = false; self.stopEvent(e); return false; } } this.handleStatusClick = function(e) { self.setPosition(e); if (!pl.hasClass(self.lastSound._data.oLI,self.css.sPaused)) self.resume(); return self.stopEvent(e); } this.stopEvent = function(e) { if (typeof e != 'undefined') { if (typeof e.preventDefault != 'undefined') { e.preventDefault(); } else if (typeof e.returnValue != 'undefined' || typeof event != 'undefined') { (e||event).cancelBubble = true; (e||event).returnValue = false; } } return false; } this.setPosition = function(e) { // called from slider control var oThis = self.getTheDamnTarget(e); if (!oThis) { return true; } var oControl = oThis; while (!self.hasClass(oControl,'controls') && oControl.parentNode) { oControl = oControl.parentNode; } var oSound = self.lastSound; var x = parseInt(e.clientX); // play sound at this position var nMsecOffset = Math.floor((x-self.getOffX(oControl)-4)/(oControl.offsetWidth)*self.getDurationEstimate(oSound)); if (!isNaN(nMsecOffset)) nMsecOffset = Math.min(nMsecOffset,oSound.duration); if (!isNaN(nMsecOffset)) oSound.setPosition(nMsecOffset); } this.stopSound = function(oSound) { sm._writeDebug('stopping sound: '+oSound.sID); sm.stop(oSound.sID); sm.unload(oSound.sID); } this.getDurationEstimate = function(oSound) { if (oSound.instanceOptions.isMovieStar) { return (oSound.duration); } else { return (!oSound._data.metadata || !oSound._data.metadata.data.givenDuration?(oSound.durationEstimate||0):oSound._data.metadata.data.givenDuration); } } this.createVUData = function() { var i=0; var j=0; var canvas = vuDataCanvas.getContext('2d'); var vuGrad = canvas.createLinearGradient(0, 16, 0, 0); vuGrad.addColorStop(0,'rgb(0,192,0)'); vuGrad.addColorStop(0.30,'rgb(0,255,0)'); vuGrad.addColorStop(0.625,'rgb(255,255,0)'); vuGrad.addColorStop(0.85,'rgb(255,0,0)'); var bgGrad = canvas.createLinearGradient(0, 16, 0, 0); var outline = 'rgba(0,0,0,0.2)'; bgGrad.addColorStop(0,outline); bgGrad.addColorStop(1,'rgba(0,0,0,0.5)'); for (i=0; i<16; i++) { self.vuMeterData[i] = []; } for (var i=0; i<16; i++) { for (j=0; j<16; j++) { // reset/erase canvas vuDataCanvas.setAttribute('width',16); vuDataCanvas.setAttribute('height',16); // draw new stuffs canvas.fillStyle = bgGrad; canvas.fillRect(0,0,7,15); canvas.fillRect(8,0,7,15); /* // shadow canvas.fillStyle = 'rgba(0,0,0,0.1)'; canvas.fillRect(1,15-i,7,17-(17-i)); canvas.fillRect(9,15-j,7,17-(17-j)); */ canvas.fillStyle = vuGrad; canvas.fillRect(0,15-i,7,16-(16-i)); canvas.fillRect(8,15-j,7,16-(16-j)); // and now, clear out some bits. canvas.clearRect(0,3,16,1); canvas.clearRect(0,7,16,1); canvas.clearRect(0,11,16,1); self.vuMeterData[i][j] = vuDataCanvas.toDataURL('image/png'); // for debugging VU images /* var o = document.createElement('img'); o.style.marginRight = '5px'; o.src = self.vuMeterData[i][j]; document.documentElement.appendChild(o); */ } } }; var vuDataCanvas = null; this.testCanvas = function() { // canvas + toDataURL(); var c = document.createElement('canvas'); var ctx = null; if (!c || typeof c.getContext == 'undefined') { return null; } ctx = c.getContext('2d'); if (!ctx || typeof c.toDataURL != 'function') { return null; } // just in case.. try { var ok = c.toDataURL('image/png'); } catch(e) { // no canvas or no toDataURL() return null; } // assume we're all good. return c; } if (this.config.useFavIcon) { vuDataCanvas = self.testCanvas(); if (vuDataCanvas && (isFirefox || isOpera)) { // these browsers support dynamically-updating the favicon self.createVUData(); } else { // browser doesn't support doing this this.config.useFavIcon = false; } } this.init = function() { sm._writeDebug('pagePlayer.init()'); var oLinks = document.getElementsByTagName('a'); // grab all links, look for .mp3 var foundItems = 0; for (var i=0; i0) { var oTiming = document.getElementById('sm2_timing'); self.strings['timing'] = oTiming.innerHTML; oTiming.innerHTML = ''; oTiming.id = ''; self.addEventHandler(document,'click',self.handleClick); if (!isTouchDevice) { self.addEventHandler(document,'mousedown',self.handleMouseDown); self.addEventHandler(document,'mouseup',self.stopDrag); } else { self.addEventHandler(document,'touchstart',self.handleMouseDown); self.addEventHandler(document,'touchend',self.stopDrag); } // self.addEventHandler(window,'unload',function(){}); // force page reload when returning here via back button (Opera tries to remember old state, etc.) } sm._writeDebug('pagePlayer.init(): Found '+foundItems+' relevant items.'); if (self.config.autoStart) { pl.handleClick({target:pl.links[0]}); } } var Metadata = function(oSound) { var self = this; var oLI = oSound._data.oLI; var o = oLI.getElementsByTagName('ul')[0]; var oItems = o.getElementsByTagName('li'); var oTemplate = document.createElement('div'); oTemplate.innerHTML = ' '; oTemplate.className = 'annotation'; var oTemplate2 = document.createElement('div'); oTemplate2.innerHTML = ' '; oTemplate2.className = 'annotation alt'; var oTemplate3 = document.createElement('div'); oTemplate3.className = 'note'; this.totalTime = 0; this.strToTime = function(sTime) { var segments = sTime.split(':'); var seconds = 0; for (var i=segments.length; i--;) { seconds += parseInt(segments[i])*Math.pow(60,segments.length-1-i,10); // hours, minutes } return seconds; } this.data = []; this.data.givenDuration = null; this.data.currentItem = null; this.data.mainTitle = oSound._data.oLink.innerHTML; for (var i=0; i= 9) { var lists = self.getElementsByClassName('playlist','ul',document.documentElement); for (var i=lists.length; i--;) { self.addClass(lists[i],self.cssBase); } var sbC = sb.getElementsByTagName('div')[0]; var oF = document.createDocumentFragment(); var oClone = null; for (i=256; i--;) { oClone = sbC.cloneNode(false); oClone.style.left = (i)+'px'; oF.appendChild(oClone); } sb.removeChild(sbC); sb.appendChild(oF); } this.oControls = document.getElementById('control-template').cloneNode(true); this.oControls.id = ''; this.init(); } } var pagePlayer = new PagePlayer(typeof PP_CONFIG != 'undefined'?PP_CONFIG:null); soundManager.onready(function() { if (soundManager.supported()) { // soundManager.createSound() etc. may now be called pagePlayer.initDOM(); } });