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: Robert M. Hall
10 * Date: 20th November 2014
11 * Based on JplayerMp4.as with modifications for rtmp
12 */
13
14package happyworm.jPlayer
15{
16	import flash.display.Sprite;
17
18	import flash.media.Video;
19	import flash.media.SoundTransform;
20
21	import flash.net.NetConnection;
22	import flash.net.NetStream;
23	import flash.net.Responder;
24
25	import flash.utils.Timer;
26	import flash.utils.getTimer;
27
28	import flash.events.NetStatusEvent;
29	import flash.events.SecurityErrorEvent;
30	import flash.events.TimerEvent;
31	import flash.events.ErrorEvent;
32	import flash.events.UncaughtErrorEvent;
33	import flash.utils.clearInterval;
34	import flash.utils.setInterval;
35	import happyworm.jPlayer.ConnectManager;
36
37	public class JplayerRtmp extends Sprite
38	{
39
40		public var myVideo:Video = new Video;
41		private var myConnection:NetConnection;
42		private var myStream:NetStream;
43
44		public var responder:Responder;
45
46		private var streamName:String;
47
48		private var connectString:Object;
49
50		private var firstTime:Boolean = true;
51
52		private var myTransform:SoundTransform = new SoundTransform  ;
53
54		public var myStatus:JplayerStatus = new JplayerStatus  ;
55
56		private var ConnMgr:ConnectManager=new ConnectManager();
57
58		private var timeUpdateTimer:Timer = new Timer(250,0);// Matched to HTML event freq
59		private var progressTimer:Timer = new Timer(250,0);// Matched to HTML event freq
60		private var seekingTimer:Timer = new Timer(100,0);// Internal: How often seeking is checked to see if it is over.
61
62		private var startBuffer:Number = 3;
63		private var maxBuffer:Number = 12;
64
65		public function JplayerRtmp(volume:Number)
66		{
67			myConnection = new NetConnection  ;
68			myConnection.client = this;
69
70
71			// Moved the netconnection negotiation into the ConnectManager.as class - not needed for initial connection
72			// may need to add eventHandler back in for errors only or just dispatch from the ConnectManager..revisit...
73
74			// myConnection.addEventListener(NetStatusEvent.NET_STATUS,netStatusHandler);
75			// myConnection.addEventListener(SecurityErrorEvent.SECURITY_ERROR,securityErrorHandler);
76			myVideo.smoothing = true;
77			this.addChild(myVideo);
78
79			timeUpdateTimer.addEventListener(TimerEvent.TIMER,timeUpdateHandler);
80			progressTimer.addEventListener(TimerEvent.TIMER,progressHandler);
81			seekingTimer.addEventListener(TimerEvent.TIMER,seekingHandler);
82
83			myStatus.volume = volume;
84
85			addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, uncaughtErrorHandler);
86
87
88		}
89
90
91
92        private function uncaughtErrorHandler(event:UncaughtErrorEvent):void
93        {
94			trace("UNCAUGHT ERROR - try loading again");
95
96            if (event.error is Error)
97            {
98                var error:Error = event.error as Error;
99				trace(error);
100                // do something with the error
101            }
102            else if (event.error is ErrorEvent)
103            {
104                var errorEvent:ErrorEvent = event.error as ErrorEvent;
105                // do something with the error
106				trace(errorEvent);
107            }
108            else
109            {
110                // a non-Error, non-ErrorEvent type was thrown and uncaught
111            }
112			load();
113        }
114
115
116
117		private function progressUpdates(active:Boolean):void
118		{
119			if (active)
120			{
121				progressTimer.start();
122			}
123			else
124			{
125				progressTimer.stop();
126			}
127		}
128
129		private function progressHandler(e:TimerEvent):void
130		{
131			if (myStatus.isLoading)
132			{
133				if ((getLoadRatio() == 1))
134				{// Close as can get to a loadComplete event since client.onPlayStatus only works with FMS
135					this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"progressHandler: loadComplete"));
136					myStatus.loaded();
137					progressUpdates(false);
138				}
139			}
140			progressEvent();
141		}
142
143		private function progressEvent():void
144		{
145			// temporarily disabled progress event dispatching - not really needed for rtmp
146			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"progressEvent:"));
147			updateStatusValues();
148			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PROGRESS,myStatus));
149		}
150
151		private function timeUpdates(active:Boolean):void
152		{
153			if (active)
154			{
155				timeUpdateTimer.start();
156			}
157			else
158			{
159				timeUpdateTimer.stop();
160			}
161		}
162
163		private function timeUpdateHandler(e:TimerEvent):void
164		{
165			timeUpdateEvent();
166		}
167
168		private function timeUpdateEvent():void
169		{
170			updateStatusValues();
171			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_TIMEUPDATE,myStatus));
172		}
173		private function seeking(active:Boolean):void
174		{
175			if (active)
176			{
177				if (! myStatus.isSeeking)
178				{
179					seekingEvent();
180				}
181				seekingTimer.start();
182			}
183			else
184			{
185				if (myStatus.isSeeking)
186				{
187					seekedEvent();
188				}
189				seekingTimer.stop();
190			}
191		}
192		private function seekingHandler(e:TimerEvent):void
193		{
194			if ((getSeekTimeRatio() <= getLoadRatio()))
195			{
196				seeking(false);
197				if (myStatus.playOnSeek)
198				{
199					myStatus.playOnSeek = false;// Capture the flag.
200					play(myStatus.pausePosition);// Must pass time or the seek time is never set.
201				}
202				else
203				{
204					pause(myStatus.pausePosition);// Must pass time or the stream.time is read.
205				}
206			}
207			else if (myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration)
208			{
209				// Illegal seek time
210				seeking(false);
211				pause(0);
212			}
213		}
214		private function seekingEvent():void
215		{
216			myStatus.isSeeking = true;
217			updateStatusValues();
218			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKING,myStatus));
219		}
220		private function seekedEvent():void
221		{
222			myStatus.isSeeking = false;
223			updateStatusValues();
224			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKED,myStatus));
225		}
226
227
228		private function netStatusHandler(e:NetStatusEvent):void
229		{
230			trace(("netStatusHandler: " + e.info.code));
231			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"netStatusHandler: '" + e.info.code + "'"));
232			//trace("BEFORE: bufferTime: "+myStream.bufferTime+" - bufferTimeMax: "+myStream.bufferTimeMax);
233			switch (e.info.code)
234			{
235				case "NetConnection.Connect.Success" :
236					// connectStream(); // This method did not do anything sensible anyway.
237					// Do not think this case occurs. This was for the static file connection.
238					// Which now seems to be handled by the Connection Manager.
239					break;
240				case "NetStream.Buffer.Full":
241				if(connectString.streamTYPE == "LIVE") {
242						myStream.bufferTime = startBuffer;
243					} else {
244						myStream.bufferTime = maxBuffer;
245					}
246					break;
247				case "NetStream.Buffer.Flush":
248					myStream.bufferTime = startBuffer;
249					break;
250				case "NetStream.Buffer.Empty":
251					myStream.bufferTime = startBuffer;
252					break;
253				case "NetStream.Seek.Notify":
254					myStream.bufferTime = startBuffer;
255					break;
256				case "NetStream.Play.Start" :
257
258					if (firstTime) {
259						firstTime = false; // Capture flag
260
261						myStatus.loading();
262						this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART,myStatus));
263
264						// NB: With MP4 player both audio and video types get connected to myVideo.
265						// NB: Had believed it was important for the audio too, otherwise what plays it?
266						if(videoBinding) {
267							myVideo.attachNetStream(myStream);
268						}
269
270						setVolume(myStatus.volume);
271
272						// Really the progress event just needs to be generated once, and should probably happen before now.
273						progressUpdates(true);
274
275						// This is an ASSUMPTION! Made it so that the default GUI worked.
276						// Hence why this part should be refactored.
277						// Lots of commands sequences after setMedia would be corrupted by this assumption.
278						// Bascally, we assume that after a setMedia, you will play it.
279						// Doing setMedia and pause(15) cause the flag to be set incorrectly and the GUI locks up.
280						myStatus.isPlaying = true; // Should be handled elsewhere.
281					}
282
283					// Under RTMP, this event code occurs every time the media starts playing and when a new position is seeked to, even when paused.
284
285					// Since under RTMP the event behaviour is quite different, believe a refactor is best here.
286					// ie., Under RTMP we should be able to know a lot more info about the stream.
287
288					// See onMetaDataHandler() for other condition, since duration is vital.
289					// See onResult() response handler too.
290					// Appears to be some duplication between onMetaDataHandler() and onResult(), along with a race between them occuring.
291
292					break;
293				case "NetStream.Play.UnpublishNotify":
294					myStream.bufferTime = startBuffer; // was 3
295				case "NetStream.Play.Stop" :
296					myStream.bufferTime = startBuffer; // was 3
297					//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"NetStream.Play.Stop: getDuration() - getCurrentTime() = " + (getDuration() - getCurrentTime())));
298
299					// Check if media is at the end (or close) otherwise this was due to download bandwidth stopping playback. ie., Download is not fast enough.
300					if (Math.abs((getDuration() - getCurrentTime())) < 150)
301					{// Testing found 150ms worked best for M4A files, where playHead(99.9) caused a stuck state due to firing with ~116ms left to play.
302						//endedEvent();
303					}
304					break;
305				case "NetStream.Seek.InvalidTime" :
306					// Used for capturing invalid set times and clicks on the end of the progress bar.
307					endedEvent();
308					break;
309				case "NetStream.Play.StreamNotFound" :
310					myStatus.error();
311					// Resets status except the src, and it sets srcError property.;
312					this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ERROR,myStatus));
313					break;
314			}
315			//trace("AFTER: bufferTime: "+myStream.bufferTime+" - bufferTimeMax: "+myStream.bufferTimeMax);
316			// "NetStream.Seek.Notify" event code is not very useful. It occurs after every seek(t) command issued and does not appear to wait for the media to be ready.
317		}
318		private function endedEvent():void
319		{
320			trace("ENDED STREAM EVENT");
321			var wasPlaying:Boolean = myStatus.isPlaying;
322
323			// timeUpdates(false);
324			// timeUpdateEvent();
325			pause(0);
326
327			if (wasPlaying)
328			{
329				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ENDED,myStatus));
330			}
331		}
332		private function securityErrorHandler(event:SecurityErrorEvent):void
333		{
334			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"securityErrorHandler."));
335		}
336		public function connectStream():void
337		{
338			trace("CONNECTING");
339			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"connectStream."));
340			//this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAY,myStatus));
341
342			timeUpdates(true);
343			progressUpdates(true);
344			//myVideo.attachNetStream(myStream);
345			//setVolume(myStatus.volume);
346		}
347
348		private function onResult(result:Object):void
349		{
350			trace("OnResult EVENT FIRED!");
351			myStatus.duration = parseFloat(result.toString()) * 1000;
352			trace((("The stream length is " + result) + " seconds"));
353
354			if(!myConnection.connected) {
355				load();
356			} else {
357				//this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAY,myStatus,"Rockit!"));
358
359			//myStatus.loaded();
360			//myStatus.isPlaying=true;
361			if (! myStatus.metaDataReady)
362			{
363				//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"onMetaDataHandler: " + myStatus.duration));
364
365				//  Allow multiple onResult Handlers to affect size. As per PR #131 and #98.
366				//  myStatus.metaDataReady = true;
367
368				/*var info:Object = new Object();
369				info.duration=myStatus.duration
370				info.width=undefined;
371				info.height=undefined;
372				myStatus.metaData = info;
373				*/
374				if (myStatus.playOnLoad)
375				{
376					myStatus.playOnLoad = false;// Capture the flag
377					if (myStatus.pausePosition > 0)
378					{// Important for setMedia followed by play(time).
379						play(myStatus.pausePosition);
380					}
381					else
382					{
383						play();// Not always sending pausePosition avoids the extra seek(0) for a normal play() command.
384					}
385
386				}
387				else
388				{
389					pause(myStatus.pausePosition);// Always send the pausePosition. Important for setMedia() followed by pause(time). Deals with not reading stream.time with setMedia() and play() immediately followed by stop() or pause(0)
390				}
391				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADEDMETADATA,myStatus));
392			}
393			else
394			{
395				//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"onMetaDataHandler: Already read (NO EFFECT)"));
396			}
397
398			myStream.play(streamName);
399			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAY,myStatus));
400			// timeUpdates(false);
401			}
402
403		}
404
405		private var overRideConnect:Boolean=false;
406		public function doneYet():void {
407			if(!myConnection.connected) {
408				// try again
409				ConnMgr.stopAll(true);
410				overRideConnect=true;
411				trace("Connected: "+myConnection.connected+" - "+myStatus.loadRequired());
412				load();
413			}
414		}
415
416		private var videoBinding:Boolean=false;
417		public function setFile(src:String,videoSupport:Boolean=false):void
418		{
419			// videoSupport turns on/off video - by default no video, audio only
420			videoBinding=videoSupport;
421			/* Dont close the stream or netconnection here anymore so we can recycle if host/appname are the same
422			if ((myStream != null))
423			{
424				myStream.close();
425				myConnection.close();
426			}
427			*/
428			if(ConnMgr.getNegotiating() == true) {
429			    //ConnMgr.stopAll();
430				ConnMgr.setNegotiating(false);
431			}
432
433			myVideo.clear();
434
435			progressUpdates(false);
436			timeUpdates(false);
437
438			myStatus.reset();
439			myStatus.src = src;
440			myStatus.srcSet = true;
441
442			firstTime = true;
443
444			//myStatus.loaded();
445
446			if(src != "") {
447				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAY,myStatus));
448			}
449
450			//timeUpdateEvent();
451		}
452
453		public function shutDownNcNs():void {
454			trace("Connections Closed");
455			timeUpdates(false);
456			progressUpdates(false);
457			myStream.close();
458			myConnection.close();
459
460			myStatus.reset();
461			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ENDED,myStatus));
462		}
463
464		public function clearFile():void
465		{
466			if (myStream != null)
467			{
468				myStream.close();
469				// Dont close the netConnection here any longer, as we may recycle it later
470				// may need an extra way to close manually if switching media types after an rtmp session - revisit
471				// myConnection.close();
472				myStatus.reset();
473			}
474			setFile("");
475			myStatus.srcSet = false;
476		}
477
478		public function parseRTMPsrcConnect(rtmpSrc:String):Object
479		{
480			// rtmp://cp76372.live.edgefcs.net/live/Flash1Office@60204
481			var appName:String = "";
482			var streamFileName:String = "";
483			var startIndex:uint = 2 + rtmpSrc.indexOf("//");
484			var streamTYPE:String = "recorded";
485			var host:String = rtmpSrc.substr(startIndex);
486			var port:String = "";
487			host = host.substr(0,host.indexOf("/"));
488			var endHost:Number = startIndex + host.length + 1;
489
490			// See if there is a host port specified
491			if(host.indexOf(":") != -1) {
492				port = host.substr(host.indexOf(":")+1);
493				host = host.substr(0,host.indexOf(":"));
494			}
495
496			// Akamai specific live streams
497			if (rtmpSrc.lastIndexOf("/live/") != -1)
498			{
499				trace("LIVE!");
500
501
502				appName = rtmpSrc.substring(endHost,rtmpSrc.lastIndexOf("/live/") + 6);
503				streamFileName = rtmpSrc.substr(rtmpSrc.lastIndexOf("/live/") + 6);
504				streamTYPE="LIVE";
505			} else {
506				streamTYPE="RECORDED";
507
508			}
509
510			// Mp3 streams with standard appname/no instance name, mp3: prefix
511			if (rtmpSrc.indexOf("mp3:") != -1) {
512				appName = rtmpSrc.substring(endHost,rtmpSrc.indexOf("mp3:"));
513				streamFileName = rtmpSrc.substr(rtmpSrc.indexOf("mp3:"));
514				if ( streamFileName.indexOf("?") != -1 ) {
515					var tmp:String = streamFileName.substring(streamFileName.indexOf("?")) ;
516					streamFileName = streamFileName.substr(0,streamFileName.indexOf("?")) + encodeURI(tmp) ;
517				} else {
518					streamFileName = streamFileName.substr(0,streamFileName.length - 4);
519				}
520			}
521			// rtmp://cp83813.edgefcs.net/ondemand/rob_hall/bruce_campbell_oldspice.flv
522
523			// Mp4 streams with standard appname/no instance name, mp4: prefix
524			if (rtmpSrc.indexOf("mp4:") != -1) {
525				appName = rtmpSrc.substring(endHost,rtmpSrc.indexOf("mp4:"));
526				streamFileName = rtmpSrc.substr(rtmpSrc.indexOf("mp4:"));
527				if ( streamFileName.indexOf("?") != -1 ) {
528					var tmpV:String = streamFileName.substring(streamFileName.indexOf("?")) ;
529					streamFileName = streamFileName.substr(0,streamFileName.indexOf("?")) + encodeURI(tmpV) ;
530				} else {
531					streamFileName = streamFileName.substr(0,streamFileName.length - 4);
532				}
533			}
534
535			// .f4v streams with standard appname/no instance name, .flv extension
536			if (rtmpSrc.indexOf(".flv") != -1)
537			{
538			// allow use of ^ in rtmp string to indicate break point for an appname or instance name that
539			// contains a / in it where it would require multiple connection attempts or manual configuratiom
540			// of the appname/instancename
541			var endApp:Number=0;
542			if(rtmpSrc.indexOf("^") != -1) {
543				endApp=rtmpSrc.indexOf("^");
544				rtmpSrc.replace("^", "/");
545			} else {
546				endApp = rtmpSrc.indexOf("/",endHost);
547			}
548				appName = rtmpSrc.substring(endHost,endApp) + "/";
549				streamFileName = rtmpSrc.substr(endApp+1);
550			}
551
552			if(port=="") {
553				port="MULTI";
554			}
555			//rtmp, rtmpt, rtmps, rtmpe, rtmpte
556
557
558			trace(("\n\n*** HOST: " + host));
559			trace(("*** PORT: " + port));
560			trace(("*** APPNAME: " + appName));
561			trace(("*** StreamName: " + streamFileName));
562
563			var streamParts:Object = new Object;
564			streamParts.streamTYPE=streamTYPE;
565			streamParts.appName = appName;
566			streamParts.streamFileName = streamFileName;
567			streamParts.hostName = host;
568			streamName = streamFileName;
569
570			return streamParts;
571		}
572
573		public function load():Boolean
574		{
575			//trace("LOAD: "+myStatus.src);
576			if (myStatus.loadRequired() || overRideConnect==true)
577			{
578				overRideConnect=false;
579				myStatus.startingDownload();
580				var lastAppName:String;
581				var lastHostName:String;
582
583				try{
584					// we do a try, as these properties might not exist yet
585				if(connectString.appName != "" && connectString.appName != undefined) {
586					trace("PREVIOUS APP/HOST INFO AVAILABLE");
587					lastAppName = connectString.appName;
588					lastHostName = connectString.hostName;
589					trace("LAST: "+lastAppName,lastHostName);
590				}
591				} catch (error:Error) {
592					//trace("*** Caught an error condition: "+error);
593				}
594
595				connectString = parseRTMPsrcConnect(myStatus.src);
596
597
598
599				trace("**** LOAD :: CONNECT SOURCE: " +connectString.hostName +" "+ connectString.appName);
600				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_WAITING, myStatus));
601
602				if((connectString.appName == lastAppName && connectString.hostName == lastHostName) && (myConnection.connected)) {
603					// recycle the netConnection
604					trace("RECYCLING NETCONNECTION");
605					if ((myStream != null))
606					{
607						myStream.close();
608					}
609					connectStream();
610					onBWDone(null,myConnection);
611				} else {
612					// myConnection.connect(connectString.appName);
613					trace("NEW NetConnection Negotiation");
614					if ((myStream != null))
615					{
616						myStream.close();
617						myConnection.close();
618					}
619
620					ConnMgr.stopAll(true);
621					ConnMgr.negotiateConnect(this,connectString.hostName,connectString.appName);
622				}
623
624				trace("**** LOAD2 :: CONNECT SOURCE: " +connectString.hostName +" "+ connectString.appName);
625				this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_WAITING, myStatus));
626				return true;
627			}
628			else
629			{
630				return false;
631			}
632		}
633
634
635
636		public function onFCUnsubscribe(info:Object):void
637		{
638			trace(("onFCUnSubscribe worked" + info));
639		}
640
641		public function onFCSubscribe(info:Object):void
642		{
643			trace(("onFCSubscribe worked" + info));
644		}
645
646		public function onBWDone(info:Object,nc:NetConnection):void
647		{
648			if(nc.connected) {
649			myConnection=nc;
650			trace(((("onBWDone " + info) + " :: ") + myStatus.src));
651
652			var customClient:Object = new Object  ;
653			customClient.onMetaData = onMetaDataHandler;
654			customClient.onPlayStatus = onPlayStatusHandler;// According to the forums and my tests, onPlayStatus only works with FMS (Flash Media Server).
655
656			myStream = null;
657			myStream = new NetStream(myConnection);
658			myStream.addEventListener(NetStatusEvent.NET_STATUS,netStatusHandler);
659			myStream.client = customClient;
660				if(connectString.streamTYPE == "LIVE") {
661					myStream.bufferTime = 3; // was 3
662					myStream.bufferTimeMax = 24;
663					startBuffer = 3;
664					maxBuffer = 12;
665
666				} else {
667					myStream.bufferTime = .2; // was 3
668					myStream.bufferTimeMax = 0;
669					startBuffer = .2;
670					maxBuffer = 12;
671				}
672
673
674			//streamName="";
675			//var connectString:Object = parseRTMPsrcConnect(myStatus.src);
676			//streamName=connectString.streamFileName;
677
678			responder = new Responder(onResult);
679			myConnection.call("getStreamLength",responder,streamName);
680			} else {
681				connectStream();
682			}
683
684			trace("PLAY SOURCE: "+connectString);
685
686		}
687
688			public function play(time:Number = NaN):Boolean {
689			//trace("PLAY: "+time+" - isPlaying: "+myStatus.isPlaying +" - myStatus.isStartingDownload:"+myStatus.isStartingDownload);
690			var wasPlaying:Boolean = myStatus.isPlaying;
691
692			if(!isNaN(time) && myStatus.srcSet) {
693				if(myStatus.isPlaying) {
694					myStream.pause();
695					myStatus.isPlaying = false;
696				}
697				myStatus.pausePosition = time;
698			}
699
700			if(myStatus.isStartingDownload) {
701				myStatus.playOnLoad = true; // Raise flag, captured in onMetaDataHandler()
702				return true;
703			} else if(myStatus.loadRequired()) {
704				myStatus.playOnLoad = true; // Raise flag, captured in onMetaDataHandler()
705				return load();
706			} else if((myStatus.isLoading || myStatus.isLoaded) && !myStatus.isPlaying) {
707				if(myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration && connectString.streamTYPE != "LIVE") { // The time is invalid, ie., past the end.
708					myStream.pause(); // Since it is playing by default at this point.
709					myStatus.pausePosition = 0;
710					trace("SEEKER!");
711					myStream.seek(0);
712					timeUpdates(false);
713					timeUpdateEvent();
714					if(wasPlaying) { // For when playing and then get a play(huge)
715						this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus));
716					}
717				} else if(getSeekTimeRatio() > getLoadRatio()) { // Use an estimate based on the downloaded amount
718					myStatus.playOnSeek = true;
719					seeking(true);
720					trace("SEEKER PAUSE!");
721					myStream.pause(); // Since it is playing by default at this point.
722				} else {
723					if(!isNaN(time)) { // Avoid using seek() when it is already correct.
724						trace("SEEKER3");
725						myStream.seek(myStatus.pausePosition/1000); // Since time is in ms and seek() takes seconds
726					}
727					myStatus.isPlaying = true; // Set immediately before playing. Could affects events.
728					trace("SHOULD GET RESUME!");
729					myStream.resume();
730					timeUpdates(true);
731					if(!wasPlaying) {
732						this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAY, myStatus));
733					}
734				}
735				return true;
736			} else {
737				return false;
738			}
739		}
740
741	public function pause(time:Number=NaN):Boolean
742	{
743		//trace("PAUSE: "+time);
744		myStatus.playOnLoad = false;// Reset flag in case load/play issued immediately before this command, ie., before onMetadata() event.
745		myStatus.playOnSeek = false;// Reset flag in case play(time) issued before the command and is still seeking to time set.
746
747		var wasPlaying:Boolean = myStatus.isPlaying;
748
749
750		// To avoid possible loops with timeupdate and pause(time). A pause() does not have the problem.
751		var alreadyPausedAtTime:Boolean = false;
752		if(!isNaN(time) && myStatus.pausePosition == time) {
753			alreadyPausedAtTime = true;
754		}
755
756		trace("!isNaN: "+!isNaN(time) +" isNaN: "+isNaN(time));
757
758		// Need to wait for metadata to load before ever issuing a pause. The metadata handler will call this function if needed, when ready.
759		if (((myStream != null) && myStatus.metaDataReady))
760		{// myStream is a null until the 1st media is loaded. ie., The 1st ever setMedia being followed by a pause() or pause(t).
761
762			if(connectString.streamTYPE == "LIVE") {
763				trace("PAUSING LIVE");
764				myStream.play(false)
765			} else {
766				trace("PAUSING RECORDED");
767			myStream.pause();
768			}
769		}
770		if (myStatus.isPlaying)
771		{
772			myStatus.isPlaying = false;
773			myStatus.pausePosition = myStream.time * 1000;
774		}
775
776		if (! isNaN(time) && myStatus.srcSet)
777		{
778			myStatus.pausePosition = time;
779		}
780
781		if (wasPlaying)
782		{
783			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE,myStatus));
784		}
785
786		if (myStatus.isStartingDownload)
787		{
788			return true;
789		}
790		else if (myStatus.loadRequired())
791		{
792			if ((time > 0))
793			{// We do not want the stop() command, which does pause(0), causing a load operation.
794				return load();
795			}
796			else
797			{
798				return true;// Technically the pause(0) succeeded. ie., It did nothing, since nothing was required.
799			}
800		}
801		else if (myStatus.isLoading || myStatus.isLoaded)
802		{
803			if (myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration && connectString.streamTYPE != "LIVE" )
804			{// The time is invalid, ie., past the end.
805				myStatus.pausePosition = 0;
806
807				trace("GOT HERE!");
808				myStream.seek(0);
809				seekedEvent();// Deals with seeking effect when using setMedia() then pause(huge). NB: There is no preceeding seeking event.
810			}
811			else if (! isNaN(time))
812			{
813				if ((getSeekTimeRatio() > getLoadRatio()))
814				{// Use an estimate based on the downloaded amount
815					seeking(true);
816				}
817				else
818				{
819					if (myStatus.metaDataReady && connectString.streamTYPE != "LIVE")
820					{// Otherwise seek(0) will stop the metadata loading.
821					trace("GOT HERE TOO!");
822						myStream.seek(myStatus.pausePosition / 1000);
823					}
824				}
825			}
826			timeUpdates(false);
827			// Need to be careful with timeupdate event, otherwise a pause in a timeupdate event can cause a loop.
828			// Neither pause() nor pause(time) will cause a timeupdate loop.
829			if(wasPlaying || !isNaN(time) && !alreadyPausedAtTime) {
830				timeUpdateEvent();
831			}
832			return true;
833		}
834		else
835		{
836			return false;
837		}
838	}
839	public function playHead(percent:Number):Boolean
840	{
841		var time:Number = percent * getDuration() * getLoadRatio() / 100;
842		if (myStatus.isPlaying || myStatus.playOnLoad || myStatus.playOnSeek)
843		{
844			return play(time);
845		}
846		else
847		{
848			return pause(time);
849		}
850	}
851	public function setVolume(v:Number):void
852	{
853		myStatus.volume = v;
854		myTransform.volume = v;
855		if ((myStream != null))
856		{
857			myStream.soundTransform = myTransform;
858		}
859	}
860	private function updateStatusValues():void
861	{
862		//myStatus.seekPercent = 100 * getLoadRatio();
863		myStatus.seekPercent = 100;
864		myStatus.currentTime = getCurrentTime();
865		myStatus.currentPercentRelative = 100 * getCurrentRatioRel();
866		myStatus.currentPercentAbsolute = 100 * getCurrentRatioAbs();
867		myStatus.duration = getDuration();
868	}
869	public function getLoadRatio():Number
870	{
871		return 1;
872		/*trace("LoadRatio:"+myStream.bytesLoaded, myStream.bytesTotal);
873		if((myStatus.isLoading || myStatus.isLoaded) && myStream.bytesTotal > 0) {
874			return myStream.bytesLoaded / myStream.bytesTotal;
875		} else if (myStatus.isLoaded && myStream.bytesLoaded > 0) {
876			return 1;
877		} else {
878			return 0;
879		}
880		*/
881
882	}
883	public function getDuration():Number
884	{
885		return myStatus.duration;// Set from meta data.
886	}
887	public function getCurrentTime():Number
888	{
889		if (myStatus.isPlaying)
890		{
891			//trace(myStream.time * 1000);
892			return myStream.time * 1000; // was +1000
893		}
894		else
895		{
896			return myStatus.pausePosition;
897		}
898	}
899	public function getCurrentRatioRel():Number
900	{
901
902		if ((getCurrentRatioAbs() <= getLoadRatio()))
903		{
904			//if((getLoadRatio() > 0) && (getCurrentRatioAbs() <= getLoadRatio())) {
905			return getCurrentRatioAbs() / getLoadRatio();
906		}
907		else
908		{
909			return 0;
910		}
911	}
912	public function getCurrentRatioAbs():Number
913	{
914		if ((getDuration() > 0))
915		{
916			return getCurrentTime() / getDuration();
917		}
918		else
919		{
920			return 0;
921		}
922	}
923	public function getSeekTimeRatio():Number
924	{
925		if ((getDuration() > 0))
926		{
927			return myStatus.pausePosition / getDuration();
928		}
929		else
930		{
931			return 1;
932		}
933	}
934	public function onPlayStatusHandler(infoObject:Object):void
935	{
936		trace((("OnPlayStatusHandler called: (" + getTimer()) + " ms)"));
937		for (var prop:* in infoObject)
938		{
939			trace(((("\t" + prop) + ":\t") + infoObject[prop]));
940		}
941		if (infoObject.code == "NetStream.Play.Complete")
942		{
943			endedEvent();
944		}
945	}
946
947	public function onMetaDataHandler(info:Object):void
948	{// Used in connectStream() in myStream.client object.
949		// This event occurs when jumping to the start of static files! ie., seek(0) will cause this event to occur.
950
951		if (! myStatus.metaDataReady)
952		{
953			trace("\n\n*** METADATA FIRED! ***\n\n");
954			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"onMetaDataHandler: " + info.duration + " | " + info.width + "x" + info.height));
955
956			myStatus.metaDataReady = true;// Set flag so that this event only effects jPlayer the 1st time.
957			myStatus.metaData = info;
958			myStatus.duration = info.duration * 1000;// Only available via Meta Data.
959			if (info.width != undefined)
960			{
961				myVideo.width = myStatus.videoWidth = info.width;
962			}
963			if (info.height != undefined)
964			{
965				myVideo.height = myStatus.videoHeight = info.height;
966			}
967
968			if (myStatus.playOnLoad)
969			{
970				myStatus.playOnLoad = false;// Capture the flag
971				if (myStatus.pausePosition > 0)
972				{// Important for setMedia followed by play(time).
973					play(myStatus.pausePosition);
974				}
975				else
976				{
977					play();// Not always sending pausePosition avoids the extra seek(0) for a normal play() command.
978				}
979			}
980			else
981			{
982				pause(myStatus.pausePosition);// Always send the pausePosition. Important for setMedia() followed by pause(time). Deals with not reading stream.time with setMedia() and play() immediately followed by stop() or pause(0)
983			}
984			this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADEDMETADATA,myStatus));
985		}
986		else
987		{
988			//this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG,myStatus,"onMetaDataHandler: Already read (NO EFFECT)"));
989		}
990	}
991}
992}
993