1/*
2 * CirclePlayer for the jPlayer Plugin (jQuery)
3 * http://www.jplayer.org
4 *
5 * Copyright (c) 2009 - 2012 Happyworm Ltd
6 * Dual licensed under the MIT and GPL licenses.
7 *  - http://www.opensource.org/licenses/mit-license.php
8 *  - http://www.gnu.org/copyleft/gpl.html
9 *
10 * Version: 1.0.1 (jPlayer 2.1.0+)
11 * Date: 30th May 2011
12 *
13 * Author: Mark J Panaghiston @thepag
14 *
15 * CirclePlayer prototype developed by:
16 * Mark Boas @maboa
17 * Silvia Benvenuti @aulentina
18 * Jussi Kalliokoski @quinirill
19 *
20 * Inspired by :
21 * Neway @imneway http://imneway.net/ http://forrst.com/posts/Untitled-CPt
22 * and
23 * Liam McKay @liammckay http://dribbble.com/shots/50882-Purple-Play-Pause
24 *
25 * Standing on the shoulders of :
26 * John Resig @jresig
27 * Mark Panaghiston @thepag
28 * Louis-Rémi Babé @Louis_Remi
29 */
30
31
32var CirclePlayer = function(jPlayerSelector, media, options) {
33	var	self = this,
34
35		defaults = {
36			// solution: "flash, html", // For testing Flash with CSS3
37			supplied: "m4a, oga",
38			// Android 2.3 corrupts media element if preload:"none" is used.
39			// preload: "none", // No point preloading metadata since no times are displayed. It helps keep the buffer state correct too.
40			cssSelectorAncestor: "#cp_container_1",
41			cssSelector: {
42				play: ".cp-play",
43				pause: ".cp-pause"
44			}
45		},
46
47		cssSelector = {
48			bufferHolder: ".cp-buffer-holder",
49			buffer1: ".cp-buffer-1",
50			buffer2: ".cp-buffer-2",
51			progressHolder: ".cp-progress-holder",
52			progress1: ".cp-progress-1",
53			progress2: ".cp-progress-2",
54			circleControl: ".cp-circle-control"
55		};
56
57	this.cssClass = {
58		gt50: "cp-gt50",
59		fallback: "cp-fallback"
60	};
61
62	this.spritePitch = 104;
63	this.spriteRatio = 0.24; // Number of steps / 100
64
65	this.player = $(jPlayerSelector);
66	this.media = $.extend({}, media);
67	this.options = $.extend(true, {}, defaults, options); // Deep copy
68
69	this.cssTransforms = Modernizr.csstransforms;
70	this.audio = {};
71	this.dragging = false; // Indicates if the progressbar is being 'dragged'.
72
73	this.eventNamespace = ".CirclePlayer"; // So the events can easily be removed in destroy.
74
75	this.jq = {};
76	$.each(cssSelector, function(entity, cssSel) {
77		self.jq[entity] = $(self.options.cssSelectorAncestor + " " + cssSel);
78	});
79
80	this._initSolution();
81	this._initPlayer();
82};
83
84CirclePlayer.prototype = {
85	_createHtml: function() {
86	},
87	_initPlayer: function() {
88		var self = this;
89		this.player.jPlayer(this.options);
90
91		this.player.bind($.jPlayer.event.ready + this.eventNamespace, function(event) {
92			if(event.jPlayer.html.used && event.jPlayer.html.audio.available) {
93				self.audio = $(this).data("jPlayer").htmlElement.audio;
94			}
95			$(this).jPlayer("setMedia", self.media);
96			self._initCircleControl();
97		});
98
99		this.player.bind($.jPlayer.event.play + this.eventNamespace, function(event) {
100			$(this).jPlayer("pauseOthers");
101		});
102
103		// This event fired as play time increments
104		this.player.bind($.jPlayer.event.timeupdate + this.eventNamespace, function(event) {
105			if (!self.dragging) {
106				self._timeupdate(event.jPlayer.status.currentPercentAbsolute);
107			}
108		});
109
110		// This event fired as buffered time increments
111		this.player.bind($.jPlayer.event.progress + this.eventNamespace, function(event) {
112			var percent = 0;
113			if((typeof self.audio.buffered === "object") && (self.audio.buffered.length > 0)) {
114				if(self.audio.duration > 0) {
115					var bufferTime = 0;
116					for(var i = 0; i < self.audio.buffered.length; i++) {
117						bufferTime += self.audio.buffered.end(i) - self.audio.buffered.start(i);
118						// console.log(i + " | start = " + self.audio.buffered.start(i) + " | end = " + self.audio.buffered.end(i) + " | bufferTime = " + bufferTime + " | duration = " + self.audio.duration);
119					}
120					percent = 100 * bufferTime / self.audio.duration;
121				} // else the Metadata has not been read yet.
122				// console.log("percent = " + percent);
123			} else { // Fallback if buffered not supported
124				// percent = event.jPlayer.status.seekPercent;
125				percent = 0; // Cleans up the inital conditions on all browsers, since seekPercent defaults to 100 when object is undefined.
126			}
127			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.
128			// Firefox 4 does not always give the final progress event when buffered = 100%
129		});
130
131		this.player.bind($.jPlayer.event.ended + this.eventNamespace, function(event) {
132			self._resetSolution();
133		});
134	},
135	_initSolution: function() {
136		if (this.cssTransforms) {
137			this.jq.progressHolder.show();
138			this.jq.bufferHolder.show();
139		}
140		else {
141			this.jq.progressHolder.addClass(this.cssClass.gt50).show();
142			this.jq.progress1.addClass(this.cssClass.fallback);
143			this.jq.progress2.hide();
144			this.jq.bufferHolder.hide();
145		}
146		this._resetSolution();
147	},
148	_resetSolution: function() {
149		if (this.cssTransforms) {
150			this.jq.progressHolder.removeClass(this.cssClass.gt50);
151			this.jq.progress1.css({'transform': 'rotate(0deg)'});
152			this.jq.progress2.css({'transform': 'rotate(0deg)'}).hide();
153		}
154		else {
155			this.jq.progress1.css('background-position', '0 ' + this.spritePitch + 'px');
156		}
157	},
158	_initCircleControl: function() {
159		var self = this;
160		this.jq.circleControl.grab({
161			onstart: function(){
162				self.dragging = true;
163			}, onmove: function(event){
164				var pc = self._getArcPercent(event.position.x, event.position.y);
165				self.player.jPlayer("playHead", pc).jPlayer("play");
166				self._timeupdate(pc);
167			}, onfinish: function(event){
168				self.dragging = false;
169				var pc = self._getArcPercent(event.position.x, event.position.y);
170				self.player.jPlayer("playHead", pc).jPlayer("play");
171			}
172		});
173	},
174	_timeupdate: function(percent) {
175		var degs = percent * 3.6+"deg";
176
177		var spriteOffset = (Math.floor((Math.round(percent))*this.spriteRatio)-1)*-this.spritePitch;
178
179		if (percent <= 50) {
180			if (this.cssTransforms) {
181				this.jq.progressHolder.removeClass(this.cssClass.gt50);
182				this.jq.progress1.css({'transform': 'rotate(' + degs + ')'});
183				this.jq.progress2.hide();
184			} else { // fall back
185				this.jq.progress1.css('background-position', '0 '+spriteOffset+'px');
186			}
187		} else if (percent <= 100) {
188			if (this.cssTransforms) {
189				this.jq.progressHolder.addClass(this.cssClass.gt50);
190				this.jq.progress1.css({'transform': 'rotate(180deg)'});
191				this.jq.progress2.css({'transform': 'rotate(' + degs + ')'});
192				this.jq.progress2.show();
193			} else { // fall back
194				this.jq.progress1.css('background-position', '0 '+spriteOffset+'px');
195			}
196		}
197	},
198	_progress: function(percent) {
199		var degs = percent * 3.6+"deg";
200
201		if (this.cssTransforms) {
202			if (percent <= 50) {
203				this.jq.bufferHolder.removeClass(this.cssClass.gt50);
204				this.jq.buffer1.css({'transform': 'rotate(' + degs + ')'});
205				this.jq.buffer2.hide();
206			} else if (percent <= 100) {
207				this.jq.bufferHolder.addClass(this.cssClass.gt50);
208				this.jq.buffer1.css({'transform': 'rotate(180deg)'});
209				this.jq.buffer2.show();
210				this.jq.buffer2.css({'transform': 'rotate(' + degs + ')'});
211			}
212		}
213	},
214	_getArcPercent: function(pageX, pageY) {
215		var	offset	= this.jq.circleControl.offset(),
216			x	= pageX - offset.left - this.jq.circleControl.width()/2,
217			y	= pageY - offset.top - this.jq.circleControl.height()/2,
218			theta	= Math.atan2(y,x);
219
220		if (theta > -1 * Math.PI && theta < -0.5 * Math.PI) {
221			theta = 2 * Math.PI + theta;
222		}
223
224		// theta is now value between -0.5PI and 1.5PI
225		// ready to be normalized and applied
226
227		return (theta + Math.PI / 2) / 2 * Math.PI * 10;
228	},
229	setMedia: function(media) {
230		this.media = $.extend({}, media);
231		this.player.jPlayer("setMedia", this.media);
232	},
233	play: function(time) {
234		this.player.jPlayer("play", time);
235	},
236	pause: function(time) {
237		this.player.jPlayer("pause", time);
238	},
239	destroy: function() {
240		this.player.unbind(this.eventNamespace);
241		this.player.jPlayer("destroy");
242	}
243};
244