/* * CirclePlayer for the jPlayer Plugin (jQuery) * http://www.jplayer.org * * Copyright (c) 2009 - 2012 Happyworm Ltd * Dual licensed under the MIT and GPL licenses. * - http://www.opensource.org/licenses/mit-license.php * - http://www.gnu.org/copyleft/gpl.html * * Version: 1.0.1 (jPlayer 2.1.0+) * Date: 30th May 2011 * * Author: Mark J Panaghiston @thepag * * CirclePlayer prototype developed by: * Mark Boas @maboa * Silvia Benvenuti @aulentina * Jussi Kalliokoski @quinirill * * Inspired by : * Neway @imneway http://imneway.net/ http://forrst.com/posts/Untitled-CPt * and * Liam McKay @liammckay http://dribbble.com/shots/50882-Purple-Play-Pause * * Standing on the shoulders of : * John Resig @jresig * Mark Panaghiston @thepag * Louis-Rémi Babé @Louis_Remi */ var CirclePlayer = function(jPlayerSelector, media, options) { var self = this, defaults = { // solution: "flash, html", // For testing Flash with CSS3 supplied: "m4a, oga", // Android 2.3 corrupts media element if preload:"none" is used. // preload: "none", // No point preloading metadata since no times are displayed. It helps keep the buffer state correct too. cssSelectorAncestor: "#cp_container_1", cssSelector: { play: ".cp-play", pause: ".cp-pause" } }, cssSelector = { bufferHolder: ".cp-buffer-holder", buffer1: ".cp-buffer-1", buffer2: ".cp-buffer-2", progressHolder: ".cp-progress-holder", progress1: ".cp-progress-1", progress2: ".cp-progress-2", circleControl: ".cp-circle-control" }; this.cssClass = { gt50: "cp-gt50", fallback: "cp-fallback" }; this.spritePitch = 104; this.spriteRatio = 0.24; // Number of steps / 100 this.player = jQuery(jPlayerSelector); this.media = jQuery.extend({}, media); this.options = jQuery.extend(true, {}, defaults, options); // Deep copy this.cssTransforms = Modernizr.csstransforms; this.audio = {}; this.dragging = false; // Indicates if the progressbar is being 'dragged'. this.eventNamespace = ".CirclePlayer"; // So the events can easily be removed in destroy. this.jq = {}; jQuery.each(cssSelector, function(entity, cssSel) { self.jq[entity] = jQuery(self.options.cssSelectorAncestor + " " + cssSel); }); this._initSolution(); this._initPlayer(); }; CirclePlayer.prototype = { _createHtml: function() { }, _initPlayer: function() { var self = this; this.player.jPlayer(this.options); this.player.bind(jQuery.jPlayer.event.ready + this.eventNamespace, function(event) { if(event.jPlayer.html.used && event.jPlayer.html.audio.available) { self.audio = jQuery(this).data("jPlayer").htmlElement.audio; } jQuery(this).jPlayer("setMedia", self.media); self._initCircleControl(); }); this.player.bind(jQuery.jPlayer.event.play + this.eventNamespace, function(event) { jQuery(this).jPlayer("pauseOthers"); }); // This event fired as play time increments this.player.bind(jQuery.jPlayer.event.timeupdate + this.eventNamespace, function(event) { if (!self.dragging) { self._timeupdate(event.jPlayer.status.currentPercentAbsolute); } }); // This event fired as buffered time increments this.player.bind(jQuery.jPlayer.event.progress + this.eventNamespace, function(event) { var percent = 0; if((typeof self.audio.buffered === "object") && (self.audio.buffered.length > 0)) { if(self.audio.duration > 0) { var bufferTime = 0; for(var i = 0; i < self.audio.buffered.length; i++) { bufferTime += self.audio.buffered.end(i) - self.audio.buffered.start(i); // console.log(i + " | start = " + self.audio.buffered.start(i) + " | end = " + self.audio.buffered.end(i) + " | bufferTime = " + bufferTime + " | duration = " + self.audio.duration); } percent = 100 * bufferTime / self.audio.duration; } // else the Metadata has not been read yet. // console.log("percent = " + percent); } else { // Fallback if buffered not supported // percent = event.jPlayer.status.seekPercent; percent = 0; // Cleans up the inital conditions on all browsers, since seekPercent defaults to 100 when object is undefined. } self._progress(percent); // Problem here at initial condition. Due to the Opera clause above of buffered.length > 0 above... Removing it means Opera's white buffer ring never shows like with polyfill. // Firefox 4 does not always give the final progress event when buffered = 100% }); this.player.bind(jQuery.jPlayer.event.ended + this.eventNamespace, function(event) { self._resetSolution(); }); }, _initSolution: function() { if (this.cssTransforms) { this.jq.progressHolder.show(); this.jq.bufferHolder.show(); } else { this.jq.progressHolder.addClass(this.cssClass.gt50).show(); this.jq.progress1.addClass(this.cssClass.fallback); this.jq.progress2.hide(); this.jq.bufferHolder.hide(); } this._resetSolution(); }, _resetSolution: function() { if (this.cssTransforms) { this.jq.progressHolder.removeClass(this.cssClass.gt50); this.jq.progress1.css({'transform': 'rotate(0deg)'}); this.jq.progress2.css({'transform': 'rotate(0deg)'}).hide(); } else { this.jq.progress1.css('background-position', '0 ' + this.spritePitch + 'px'); } }, _initCircleControl: function() { var self = this; this.jq.circleControl.grab({ onstart: function(){ self.dragging = true; }, onmove: function(event){ var pc = self._getArcPercent(event.position.x, event.position.y); self.player.jPlayer("playHead", pc).jPlayer("play"); self._timeupdate(pc); }, onfinish: function(event){ self.dragging = false; var pc = self._getArcPercent(event.position.x, event.position.y); self.player.jPlayer("playHead", pc).jPlayer("play"); } }); }, _timeupdate: function(percent) { var degs = percent * 3.6+"deg"; var spriteOffset = (Math.floor((Math.round(percent))*this.spriteRatio)-1)*-this.spritePitch; if (percent <= 50) { if (this.cssTransforms) { this.jq.progressHolder.removeClass(this.cssClass.gt50); this.jq.progress1.css({'transform': 'rotate(' + degs + ')'}); this.jq.progress2.hide(); } else { // fall back this.jq.progress1.css('background-position', '0 '+spriteOffset+'px'); } } else if (percent <= 100) { if (this.cssTransforms) { this.jq.progressHolder.addClass(this.cssClass.gt50); this.jq.progress1.css({'transform': 'rotate(180deg)'}); this.jq.progress2.css({'transform': 'rotate(' + degs + ')'}); this.jq.progress2.show(); } else { // fall back this.jq.progress1.css('background-position', '0 '+spriteOffset+'px'); } } }, _progress: function(percent) { var degs = percent * 3.6+"deg"; if (this.cssTransforms) { if (percent <= 50) { this.jq.bufferHolder.removeClass(this.cssClass.gt50); this.jq.buffer1.css({'transform': 'rotate(' + degs + ')'}); this.jq.buffer2.hide(); } else if (percent <= 100) { this.jq.bufferHolder.addClass(this.cssClass.gt50); this.jq.buffer1.css({'transform': 'rotate(180deg)'}); this.jq.buffer2.show(); this.jq.buffer2.css({'transform': 'rotate(' + degs + ')'}); } } }, _getArcPercent: function(pageX, pageY) { var offset = this.jq.circleControl.offset(), x = pageX - offset.left - this.jq.circleControl.width()/2, y = pageY - offset.top - this.jq.circleControl.height()/2, theta = Math.atan2(y,x); if (theta > -1 * Math.PI && theta < -0.5 * Math.PI) { theta = 2 * Math.PI + theta; } // theta is now value between -0.5PI and 1.5PI // ready to be normalized and applied return (theta + Math.PI / 2) / 2 * Math.PI * 10; }, setMedia: function(media) { this.media = jQuery.extend({}, media); this.player.jPlayer("setMedia", this.media); }, play: function(time) { this.player.jPlayer("play", time); }, pause: function(time) { this.player.jPlayer("pause", time); }, destroy: function() { this.player.unbind(this.eventNamespace); this.player.jPlayer("destroy"); } };