1/*
2 * jPlayer Plugin for jQuery JavaScript Library
3 * http://www.jplayer.org
4 *
5 * Copyright (c) 2009 - 2014 Happyworm Ltd
6 * Licensed under the MIT license.
7 * http://opensource.org/licenses/MIT
8 *
9 * Author: Mark J Panaghiston
10 * Date: 15th December 2013
11 */
12
13package happyworm.jPlayer {
14	import flash.display.Sprite;
15
16	import flash.media.Sound;
17	import flash.media.SoundChannel;
18	import flash.media.SoundLoaderContext;
19	import flash.media.SoundTransform;
20	import flash.net.URLRequest;
21	import flash.utils.Timer;
22	import flash.errors.IOError;
23	import flash.events.*;
24
25	public class JplayerMp3 extends Sprite {
26		private var mySound:Sound = new Sound();
27		private var myChannel:SoundChannel = new SoundChannel();
28		private var myContext:SoundLoaderContext = new SoundLoaderContext(3000, false);
29		private var myTransform:SoundTransform = new SoundTransform();
30		private var myRequest:URLRequest = new URLRequest();
31
32		private var timeUpdateTimer:Timer = new Timer(250, 0); // Matched to HTML event freq
33		private var progressTimer:Timer = new Timer(250, 0); // Matched to HTML event freq
34		private var seekingTimer:Timer = new Timer(100, 0); // Internal: How often seeking is checked to see if it is over.
35		private var playingTimer:Timer = new Timer(100, 0); // Internal: How often waiting/playing is checked.
36		private var waitingTimer:Timer = new Timer(3000, 0); // Internal: Check from loadstart to loadOpen. Generates a waiting event.
37
38		public var myStatus:JplayerStatus = new JplayerStatus();
39
40		public function JplayerMp3(volume:Number) {
41			timeUpdateTimer.addEventListener(TimerEvent.TIMER, timeUpdateHandler);
42			progressTimer.addEventListener(TimerEvent.TIMER, progressHandler);
43			seekingTimer.addEventListener(TimerEvent.TIMER, seekingHandler);
44			playingTimer.addEventListener(TimerEvent.TIMER, playingHandler);
45			waitingTimer.addEventListener(TimerEvent.TIMER, waitingHandler);
46			setVolume(volume);
47		}
48		public function setFile(src:String):void {
49			this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "setFile: " + src));
50			if(myStatus.isPlaying) {
51				myChannel.stop();
52			}
53			progressUpdates(false);
54			timeUpdates(false);
55			waitingTimer.stop();
56			try {
57				mySound.close();
58			} catch (err:IOError) {
59				// Occurs if the file is either yet to be opened or has finished downloading.
60			}
61			mySound = null;
62			mySound = new Sound();
63			mySound.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
64			mySound.addEventListener(Event.OPEN, loadOpen);
65			mySound.addEventListener(Event.COMPLETE, loadComplete);
66			myRequest = new URLRequest(src);
67			myStatus.reset();
68			myStatus.src = src;
69			myStatus.srcSet = true;
70			timeUpdateEvent();
71		}
72		public function clearFile():void {
73			setFile("");
74			myStatus.srcSet = false;
75		}
76		private function errorHandler(err:IOErrorEvent):void {
77			// MP3 player needs to stop progress and timeupdate events as they are started before the error occurs.
78			// NB: The MP4 player works differently and the error occurs before they are started.
79			progressUpdates(false);
80			timeUpdates(false);
81			myStatus.error(); // Resets status except the src, and it sets srcError property.
82			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ERROR, myStatus));
83		}
84		private function loadOpen(e:Event):void {
85			this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "loadOpen:"));
86			waitingTimer.stop();
87			myStatus.loading();
88			if(myStatus.playOnLoad) {
89				myStatus.playOnLoad = false; // Capture the flag
90				// this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus)); // So loadstart event happens before play event occurs.
91				play();
92			} else {
93				// this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus));
94				pause();
95			}
96			progressUpdates(true);
97		}
98		private function loadComplete(e:Event):void {
99			this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "loadComplete:"));
100			myStatus.loaded();
101			progressUpdates(false);
102			progressEvent();
103			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADEDMETADATA, myStatus));
104			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_DURATIONCHANGE, myStatus));
105			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAYTHROUGH, myStatus));
106		}
107		private function soundCompleteHandler(e:Event):void {
108			myStatus.pausePosition = 0;
109			myStatus.isPlaying = false;
110			timeUpdates(false);
111			timeUpdateEvent();
112			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ENDED, myStatus));
113		}
114		private function progressUpdates(active:Boolean):void {
115			// Using a timer rather than Flash's load progress event, because that event gave data at about 200Hz. The 10Hz timer is closer to HTML5 norm.
116			if(active) {
117				progressTimer.start();
118			} else {
119				progressTimer.stop();
120			}
121		}
122		private function progressHandler(e:TimerEvent):void {
123			progressEvent();
124		}
125		private function progressEvent():void {
126			this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "progressEvent:"));
127			updateStatusValues();
128			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PROGRESS, myStatus));
129		}
130		private function timeUpdates(active:Boolean):void {
131			if(active) {
132				timeUpdateTimer.start();
133				playingTimer.start();
134			} else {
135				timeUpdateTimer.stop();
136				playingTimer.stop();
137			}
138		}
139		private function timeUpdateHandler(e:TimerEvent):void {
140			timeUpdateEvent();
141		}
142		private function timeUpdateEvent():void {
143			updateStatusValues();
144			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_TIMEUPDATE, myStatus));
145		}
146		private function seeking(active:Boolean):void {
147			if(active) {
148				seekingEvent();
149				waitingEvent();
150				seekingTimer.start();
151			} else {
152				seekingTimer.stop();
153			}
154		}
155		private function seekingHandler(e:TimerEvent):void {
156			if(myStatus.pausePosition <= getDuration()) {
157				seekedEvent();
158				seeking(false);
159				if(myStatus.playOnSeek) {
160					myStatus.playOnSeek = false; // Capture the flag.
161					play();
162				}
163			} else if(myStatus.isLoaded && (myStatus.pausePosition > getDuration())) {
164				// Illegal seek time
165				seeking(false);
166				seekedEvent();
167				pause(0);
168			}
169		}
170		private function seekingEvent():void {
171			myStatus.isSeeking = true;
172			updateStatusValues();
173			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKING, myStatus));
174		}
175		private function seekedEvent():void {
176			myStatus.isSeeking = false;
177			updateStatusValues();
178			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKED, myStatus));
179		}
180		private function playingHandler(e:TimerEvent):void {
181			checkPlaying(false); // Without forcing playing event.
182		}
183		private function checkPlaying(force:Boolean):void {
184			if(mySound.isBuffering) {
185				if(!myStatus.isWaiting) {
186					waitingEvent();
187				}
188			} else {
189				if(myStatus.isWaiting || force) {
190					playingEvent();
191				}
192			}
193		}
194		private function waitingEvent():void {
195			myStatus.isWaiting = true;
196			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_WAITING, myStatus));
197		}
198		private function playingEvent():void {
199			myStatus.isWaiting = false;
200			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAY, myStatus));
201			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAYING, myStatus));
202		}
203		private function waitingHandler(e:TimerEvent):void {
204			waitingTimer.stop();
205			if(myStatus.playOnLoad) {
206				waitingEvent();
207			}
208		}
209		public function load():Boolean {
210			if(myStatus.loadRequired()) {
211				myStatus.startingDownload();
212				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus));
213				waitingTimer.start();
214				mySound.load(myRequest, myContext);
215				return true;
216			} else {
217				return false;
218			}
219		}
220		public function play(time:Number = NaN):Boolean {
221			var wasPlaying:Boolean = myStatus.isPlaying;
222
223			if(!isNaN(time) && myStatus.srcSet) {
224				if(myStatus.isPlaying) {
225					myChannel.stop();
226					myStatus.isPlaying = false;
227				}
228				myStatus.pausePosition = time;
229			}
230
231			if(myStatus.isStartingDownload) {
232				myStatus.playOnLoad = true; // Raise flag, captured in loadOpen()
233				return true;
234			} else if(myStatus.loadRequired()) {
235				myStatus.playOnLoad = true; // Raise flag, captured in loadOpen()
236				return load();
237			} else if((myStatus.isLoading || myStatus.isLoaded) && !myStatus.isPlaying) {
238				if(myStatus.isLoaded && myStatus.pausePosition > getDuration()) { // The time is invalid, ie., past the end.
239					myStatus.pausePosition = 0;
240					timeUpdates(false);
241					timeUpdateEvent();
242					if(wasPlaying) { // For when playing and then get a play(huge)
243						this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus));
244					}
245				} else if(myStatus.pausePosition > getDuration()) {
246					myStatus.playOnSeek = true;
247					seeking(true);
248				} else {
249					myStatus.isPlaying = true; // Set immediately before playing. Could affects events.
250					myChannel = mySound.play(myStatus.pausePosition);
251					myChannel.soundTransform = myTransform;
252					myChannel.addEventListener(Event.SOUND_COMPLETE, soundCompleteHandler);
253					timeUpdates(true);
254					if(!wasPlaying) {
255						this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAY, myStatus));
256					}
257					checkPlaying(true); // Force the playing event unless waiting, which will be dealt with in the playingTimer.
258				}
259				return true;
260			} else {
261				return false;
262			}
263		}
264		public function pause(time:Number = NaN):Boolean {
265			myStatus.playOnLoad = false; // Reset flag in case load/play issued immediately before this command, ie., before loadOpen() event.
266			myStatus.playOnSeek = false; // Reset flag in case play(time) issued before the command and is still seeking to time set.
267
268			var wasPlaying:Boolean = myStatus.isPlaying;
269
270			// To avoid possible loops with timeupdate and pause(time). A pause() does not have the problem.
271			var alreadyPausedAtTime:Boolean = false;
272			if(!isNaN(time) && myStatus.pausePosition == time) {
273				alreadyPausedAtTime = true;
274			}
275
276			if(myStatus.isPlaying) {
277				myStatus.isPlaying = false;
278				myChannel.stop();
279				if(myChannel.position > 0) { // Required otherwise a fast play then pause causes myChannel.position to equal zero and not the correct value. ie., When it happens leave pausePosition alone.
280					myStatus.pausePosition = myChannel.position;
281				}
282			}
283
284			if(!isNaN(time) && myStatus.srcSet) {
285				myStatus.pausePosition = time;
286			}
287
288			if(wasPlaying) {
289				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus));
290			}
291
292			if(myStatus.isStartingDownload) {
293				return true;
294			} else if(myStatus.loadRequired()) {
295				if(time > 0) { // We do not want the stop() command, which does pause(0), causing a load operation.
296					return load();
297				} else {
298					return true; // Technically the pause(0) succeeded. ie., It did nothing, since nothing was required.
299				}
300			} else if(myStatus.isLoading || myStatus.isLoaded) {
301				if(myStatus.isLoaded && myStatus.pausePosition > getDuration()) { // The time is invalid, ie., past the end.
302					myStatus.pausePosition = 0;
303				} else if(myStatus.pausePosition > getDuration()) {
304					seeking(true);
305				}
306				timeUpdates(false);
307				// Need to be careful with timeupdate event, otherwise a pause in a timeupdate event can cause a loop.
308				// Neither pause() nor pause(time) will cause a timeupdate loop.
309				if(wasPlaying || !isNaN(time) && !alreadyPausedAtTime) {
310					timeUpdateEvent();
311				}
312				return true;
313			} else {
314				return false;
315			}
316		}
317		public function playHead(percent:Number):Boolean {
318			var time:Number = percent * getDuration() / 100;
319			if(myStatus.isPlaying || myStatus.playOnLoad || myStatus.playOnSeek) {
320				return play(time);
321			} else {
322				return pause(time);
323			}
324		}
325		public function setVolume(v:Number):void {
326			myStatus.volume = v;
327			myTransform.volume = v;
328			myChannel.soundTransform = myTransform;
329		}
330		private function updateStatusValues():void {
331			myStatus.seekPercent = 100 * getLoadRatio();
332			myStatus.currentTime = getCurrentTime();
333			myStatus.currentPercentRelative = 100 * getCurrentRatioRel();
334			myStatus.currentPercentAbsolute = 100 * getCurrentRatioAbs();
335			myStatus.duration = getDuration();
336		}
337		public function getLoadRatio():Number {
338			if((myStatus.isLoading || myStatus.isLoaded) && mySound.bytesTotal > 0) {
339				return mySound.bytesLoaded / mySound.bytesTotal;
340			} else if (myStatus.isLoaded && mySound.bytesLoaded > 0) {
341				return 1;
342			} else {
343				return 0;
344			}
345		}
346		public function getDuration():Number {
347			if(mySound.length > 0) {
348				return mySound.length;
349			} else {
350				return 0;
351			}
352		}
353		public function getCurrentTime():Number {
354			if(myStatus.isPlaying) {
355				return myChannel.position;
356			} else {
357				return myStatus.pausePosition;
358			}
359		}
360		public function getCurrentRatioRel():Number {
361			if((getDuration() > 0) && (getCurrentTime() <= getDuration())) {
362				return getCurrentTime() / getDuration();
363			} else {
364				return 0;
365			}
366		}
367		public function getCurrentRatioAbs():Number {
368			return getCurrentRatioRel() * getLoadRatio();
369		}
370	}
371}
372