/**    
 * @author Jefferson González
 * @copyright 2010 Jefferson González
 *
 * @license 
 * This file is part of Jaris FLV Player.
 *
 * Jaris FLV Player is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License or GNU LESSER GENERAL 
 * PUBLIC LICENSE as published by the Free Software Foundation, either version 
 * 3 of the License, or (at your option) any later version.
 *
 * Jaris FLV Player is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License and 
 * GNU LESSER GENERAL PUBLIC LICENSE along with Jaris FLV Player.  If not, 
 * see <http://www.gnu.org/licenses/>.
 */

package jaris.player;

import flash.display.Loader;
import flash.display.MovieClip;
import flash.display.Sprite;
import flash.display.Stage;
import flash.display.StageDisplayState;
import flash.events.AsyncErrorEvent;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.FullScreenEvent;
import flash.events.IOErrorEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
import flash.events.ProgressEvent;
import flash.events.TimerEvent;
import flash.geom.Rectangle;
import flash.Lib;
import flash.media.ID3Info;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundTransform;
import flash.media.Video;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.URLRequest;
import flash.system.Capabilities;
import flash.system.Security;
import flash.ui.Keyboard;
import flash.ui.Mouse;
import flash.utils.Timer;
import jaris.events.PlayerEvents;
import jaris.utils.Utils;
import jaris.player.AspectRatio;
import jaris.player.UserSettings;

/**
 * Jaris main video player
 */
class Player extends EventDispatcher
{
	//{Member variables
	private var _stage:Stage;
	private var _movieClip:MovieClip;
	private var _connection:NetConnection;
	private var _stream:NetStream;
	private var _fullscreen:Bool;
	private var _soundMuted:Bool;
	private var _volume:Float;
	private var _bufferTime:Float;
	private var _mouseVisible:Bool;
	private var _mediaLoaded:Bool;
	private var _hideMouseTimer:Timer;
	private var _checkAudioTimer:Timer;
	private var _mediaSource:String;
	private var _type:String;
	private var _streamType:String;
	private var _server:String; //For future use on rtmp
	private var _sound:Sound;
	private var _soundChannel:SoundChannel;
	private var _id3Info:ID3Info;
	private var _video:Video;
	private var _videoWidth:Float;
	private var _videoHeight:Float;
	private var _videoMask:Sprite;
	private var _videoQualityHigh:Bool;
	private var _mediaDuration:Float;
	private var _lastTime:Float;	
	private var _lastProgress:Float;
	private var _isPlaying:Bool;
	private var _aspectRatio:Float;
	private var _currentAspectRatio:String;
	private var _originalAspectRatio:Float;
	private var _mediaEndReached:Bool;
	private var _seekPoints:Array<Float>;
	private var _downloadCompleted:Bool;
	private var _startTime:Float;
	private var _firstLoad:Bool;
	private var _stopped:Bool;
	private var _useHardWareScaling:Bool;
	private var _youtubeLoader:Loader;
	private var _userSettings:UserSettings;
	//}
	
	
	//{Constructor
	public function new() 
	{
		super();
		
		//{Main Variables Init
		_stage = Lib.current.stage;
		_movieClip = Lib.current;
		_mouseVisible = true;
		_soundMuted = false;
		_volume = 1.0;
		_bufferTime = 10;
		_fullscreen = false;
		_mediaLoaded = false;
		_hideMouseTimer = new Timer(1500);
		_checkAudioTimer = new Timer(100);
		_seekPoints = new Array();
		_downloadCompleted = false;
		_startTime = 0;
		_firstLoad = true;
		_stopped = false;
		_videoQualityHigh = false;
		_isPlaying = false;
		_streamType = StreamType.FILE;
		_type = InputType.VIDEO;
		_server = "";
		_currentAspectRatio = "original";
		_aspectRatio = 0;
		_lastTime = 0;
		_lastProgress = 0;
		_userSettings = new UserSettings();
		//}
		
		//{Initialize sound object
		_sound = new Sound();
		_sound.addEventListener(Event.COMPLETE, onSoundComplete);
        _sound.addEventListener(Event.ID3, onSoundID3);
        _sound.addEventListener(IOErrorEvent.IO_ERROR, onSoundIOError);
        _sound.addEventListener(ProgressEvent.PROGRESS, onSoundProgress);

		//}
		
		//{Initialize video and connection objects
		_connection = new NetConnection();
		_connection.client = this;
		_connection.connect(null);
		_stream = new NetStream(_connection);
		
		_video = new Video(_stage.stageWidth, _stage.stageHeight);
		
		_movieClip.addChild(_video);
		//}
		
		//Video mask so that custom menu items work
		_videoMask = new Sprite();
		_movieClip.addChild(_videoMask);
		
		//Set initial rendering to high quality
		toggleQuality();
		
		//{Initialize system event listeners
		_movieClip.addEventListener(Event.ENTER_FRAME, onEnterFrame);
		_stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
		_stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
		_stage.addEventListener(FullScreenEvent.FULL_SCREEN, onFullScreen);
		_stage.addEventListener(Event.RESIZE, onResize);
		_hideMouseTimer.addEventListener(TimerEvent.TIMER, hideMouseTimer);
		_checkAudioTimer.addEventListener(TimerEvent.TIMER, checkAudioTimer);
		_connection.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
		_connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError);
		//}
	}
	//}
	
	
	//{Timers
	/**
	 * Timer that hides the mouse pointer when it is idle and dispatch the PlayerEvents.MOUSE_HIDE
	 * @param	event
	 */
	private function hideMouseTimer(event:TimerEvent):Void
	{
		if (_fullscreen)
		{
			if (_mouseVisible)
			{
				_mouseVisible = false;
			}
			else
			{
				Mouse.hide();
				callEvents(PlayerEvents.MOUSE_HIDE);
				_hideMouseTimer.stop();
			}
		}
	}
	
	/**
	 * To check if the sound finished playing
	 * @param	event
	 */
	private function checkAudioTimer(event:TimerEvent):Void
	{
		if (_soundChannel.position + 100 >= _sound.length)
		{
			_isPlaying = false;
			_mediaEndReached = true;
			callEvents(PlayerEvents.PLAYBACK_FINISHED);
			
			_checkAudioTimer.stop();
		}
	}
	//}
	
	
	//{Events
	/**
	 * Callback after bandwidth calculation for rtmp streams
	 */
	private function onBWDone():Void
	{
		//Need to study this more
	}
	
	/**
	 * Triggers error event on rtmp connections
	 * @param	event
	 */
	private function onAsyncError(event:AsyncErrorEvent):Void
	{
		//TODO: Should trigger event for controls to display error message
		trace(event.error);
	}
	
	/**
	 * Checks if connection failed or succeed
	 * @param	event
	 */
	private function onNetStatus(event:NetStatusEvent):Void
	{
		switch (event.info.code)
		{
			case "NetConnection.Connect.Success":
				if (_streamType == StreamType.RTMP)
				{
					_stream = new NetStream(_connection);
					_stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
					_stream.bufferTime = 10;
					_stream.play(Utils.rtmpSourceParser(_mediaSource), true);
					_stream.client = this;
					if(_type == InputType.VIDEO) {_video.attachNetStream(_stream); }
				}
				callEvents(PlayerEvents.CONNECTION_SUCCESS);

			case "NetStream.Play.StreamNotFound":
				trace("Stream not found: " + _mediaSource); //Replace with a dispatch for error event
				callEvents(PlayerEvents.CONNECTION_FAILED);
				
			case "NetStream.Play.Stop":
				if (_streamType != StreamType.RTMP)
				{
					if (_isPlaying) { _stream.togglePause(); }
					_isPlaying = false;
					_mediaEndReached = true;
					callEvents(PlayerEvents.PLAYBACK_FINISHED);
				}
				
			case "NetStream.Play.Start":
				_isPlaying = true;
				_mediaEndReached = false;
				if (_stream.bytesLoaded != _stream.bytesTotal || _streamType == StreamType.RTMP)
				{
					callEvents(PlayerEvents.BUFFERING);
				}
				
			case "NetStream.Seek.Notify":
				_mediaEndReached = false;
				if (_streamType == StreamType.RTMP)
				{
					_isPlaying = true;
					callEvents(PlayerEvents.PLAY_PAUSE);
					callEvents(PlayerEvents.BUFFERING);
				}
				
			case "NetStream.Buffer.Empty":
				if (_stream.bytesLoaded != _stream.bytesTotal)
				{
					callEvents(PlayerEvents.BUFFERING);
				}
				
			case "NetStream.Buffer.Full":
				callEvents(PlayerEvents.NOT_BUFFERING);
				
			case "NetStream.Buffer.Flush":
				if (_stream.bytesLoaded == _stream.bytesTotal)
				{
					_downloadCompleted = true;
				}
		}
	}
	
	/**
	 * Proccess keyboard shortcuts
	 * @param	event
	 */
	private function onKeyDown(event:KeyboardEvent):Void
	{
		var F_KEY:UInt = 70;
		var M_KEY:UInt = 77;
		var X_KEY:UInt = 88;
		
		switch(event.keyCode)
		{
			case Keyboard.TAB:
				toggleAspectRatio();
			
			case F_KEY:
				toggleFullscreen();
				
			case M_KEY:
				toggleMute();
				
			case Keyboard.UP:
				volumeUp();
				
			case Keyboard.DOWN:
				volumeDown();
				
			case Keyboard.RIGHT:
				forward();
				
			case Keyboard.LEFT:
				rewind();
				
			case Keyboard.SPACE:
				togglePlay();
				
			case X_KEY:
				stopAndClose();
		}
	}
	
	/**
	 * IF player is full screen shows the mouse when gets hide
	 * @param	event
	 */
	private function onMouseMove(event:MouseEvent):Void
	{
		if (_fullscreen && !_mouseVisible)
		{
			if (!_hideMouseTimer.running)
			{
				_hideMouseTimer.start();
			}
			
			_mouseVisible = true;
			Mouse.show();
			
			callEvents(PlayerEvents.MOUSE_SHOW);
		}
	}
	
	/**
	 * Resize video player
	 * @param	event
	 */
	private function onResize(event:Event):Void
	{
		resizeAndCenterPlayer();
	}
	
	/**
	 * Dispath a full screen event to listeners as redraw player an takes care of some other aspects
	 * @param	event
	 */
	private function onFullScreen(event:FullScreenEvent):Void
	{
		_fullscreen = event.fullScreen;
		
		if (!event.fullScreen)
		{
			Mouse.show();
			callEvents(PlayerEvents.MOUSE_SHOW);
			_mouseVisible = true;
		}
		else
		{
			_mouseVisible = true;
			_hideMouseTimer.start();
		}
		
		resizeAndCenterPlayer();
		
		callEvents(PlayerEvents.FULLSCREEN);
	}
	
	/**
	 * Sits for any cue points available
	 * @param	data
	 * @note Planned future implementation
	 */
	private function onCuePoint(data:Dynamic):Void
	{
		
	}
	
	/**
	 * After a video is loaded this callback gets the video information at start and stores it on variables
	 * @param	data
	 */
	private function onMetaData(data:Dynamic):Void
	{
		if (_firstLoad)
		{
			_isPlaying = true;
			
			_firstLoad = false;
			if (data.width)
			{
				_videoWidth = data.width;
				_videoHeight = data.height;
			}
			else
			{
				_videoWidth = _video.width;
				_videoHeight = _video.height;
			}
			
			//Store seekpoints times
			if (data.hasOwnProperty("seekpoints")) //MP4
			{
				for (position in Reflect.fields(data.seekpoints))
				{
					_seekPoints.push(Reflect.field(data.seekpoints, position).time);
				}
			}
			else if (data.hasOwnProperty("keyframes")) //FLV
			{
				for (position in Reflect.fields(data.keyframes.times))
				{
					_seekPoints.push(Reflect.field(data.keyframes.times, position));
				}
			}
			
			_mediaLoaded = true;
			_mediaDuration = data.duration;
			_originalAspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight);
			
			if (_aspectRatio <= 0)
			{
				_aspectRatio = _originalAspectRatio;
			}
			
			callEvents(PlayerEvents.MEDIA_INITIALIZED);
			
			resizeAndCenterPlayer();
			
			//Retrieve the volume that user selected last time
			setVolume(_userSettings.getVolume());
		}
	}
	
	/**
	 * Dummy function invoked for pseudostream servers
	 * @param	data
	 */
	private function onLastSecond(data:Dynamic):Void
	{
		trace("last second pseudostream");
	}
	
	/**
	 * Broadcast Timeupdate and Duration	 
	 */	
	private function onEnterFrame(event:Event):Void
	{
		if (getDuration() > 0 && _lastTime != getCurrentTime()) 
		{
			_lastTime = getCurrentTime();
			callEvents(PlayerEvents.TIME);
		}
		
		if (getBytesLoaded() > 0 && _lastProgress < getBytesLoaded()) 
		{
			_lastProgress = getBytesLoaded();
			callEvents(PlayerEvents.PROGRESS);
		}
		
	}		
	
	
	/**
	 * Triggers when playbacks end on rtmp streaming server
	 */
	private function onPlayStatus(info:Dynamic):Void
	{
		_isPlaying = false;
		_mediaEndReached = true;
		callEvents(PlayerEvents.PLAYBACK_FINISHED);
	}
	
	/**
	 * When sound finished downloading
	 * @param	event
	 */
	private function onSoundComplete(event:Event)
	{
		_mediaDuration = _sound.length / 1000;
		_downloadCompleted = true;
		
		callEvents(PlayerEvents.MEDIA_INITIALIZED);
	}
	
	/**
	 * Mimic stream onMetaData
	 * @param	event
	 */
	private function onSoundID3(event:Event)
	{
		if (_firstLoad)
		{
			_soundChannel = _sound.play();
			_checkAudioTimer.start();

			_isPlaying = true;
			
			_firstLoad = false;
			
			_mediaLoaded = true;
			_mediaDuration = ((_sound.bytesTotal / _sound.bytesLoaded) * _sound.length) / 1000;
			_aspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight);
			_originalAspectRatio = _aspectRatio;
			_id3Info = _sound.id3;
			
			callEvents(PlayerEvents.CONNECTION_SUCCESS);
			callEvents(PlayerEvents.MEDIA_INITIALIZED);
			
			resizeAndCenterPlayer();
			
			//Retrieve the volume that user selected last time
			setVolume(_userSettings.getVolume());
		}
	}
	
	/**
	 * Dispatch connection failed event on error
	 * @param	event
	 */
	private function onSoundIOError(event:IOErrorEvent)
	{
		callEvents(PlayerEvents.CONNECTION_FAILED);
	}
	
	/**
	 * Monitor sound download progress
	 * @param	event
	 */
	private function onSoundProgress(event:ProgressEvent)
	{
		if (_sound.isBuffering)
		{
			callEvents(PlayerEvents.BUFFERING);
		}
		else
		{
			callEvents(PlayerEvents.NOT_BUFFERING);
		}
		
		_mediaDuration = ((_sound.bytesTotal / _sound.bytesLoaded) * _sound.length) / 1000;
		callEvents(PlayerEvents.MEDIA_INITIALIZED);
	}
	
	/**
	 * Initializes the youtube loader object
	 * @param	event
	 */
	private function onYouTubeLoaderInit(event:Event):Void
	{
		_youtubeLoader.content.addEventListener("onReady", onYoutubeReady);
		_youtubeLoader.content.addEventListener("onError", onYoutubeError);
		_youtubeLoader.content.addEventListener("onStateChange", onYoutubeStateChange);
		_youtubeLoader.content.addEventListener("onPlaybackQualityChange", onYoutubePlaybackQualityChange);

	}
	
	/**
	 * This event is fired when the player is loaded and initialized, meaning it is ready to receive API calls.
	 */
	private function onYoutubeReady(event:Event):Void
	{
		_movieClip.addChild(_youtubeLoader.content);
		_movieClip.setChildIndex(_youtubeLoader.content, 0);
		Reflect.field(_youtubeLoader.content, "setSize")(_stage.stageWidth, _stage.stageHeight);
		Reflect.field(_youtubeLoader.content, "loadVideoByUrl")(Utils.youtubeSourceParse(_mediaSource));
		callEvents(PlayerEvents.BUFFERING);
	}

	/**
	 * This event is fired whenever the player's state changes. Possible values are unstarted (-1), ended (0), 
	 * playing (1), paused (2), buffering (3), video cued (5). When the SWF is first loaded it will broadcast 
	 * an unstarted (-1) event. When the video is cued and ready to play it will broadcast a video cued event (5).
	 * @param	event
	 */
	private function onYoutubeStateChange(event:Event):Void
	{
		var status:UInt = Std.parseInt(Reflect.field(event, "data"));
		
		switch(status)
		{
			case -1:
				callEvents(PlayerEvents.BUFFERING);
			
			case 0:
				_isPlaying = false;
				_mediaEndReached = true;
				callEvents(PlayerEvents.PLAYBACK_FINISHED);
				
			case 1:
				if (_firstLoad)
				{
					_isPlaying = true;
					
					_videoWidth = _stage.stageWidth;
					_videoHeight = _stage.stageHeight;
				
					_firstLoad = false;
					
					_mediaLoaded = true;
					_mediaDuration = Reflect.field(_youtubeLoader.content, "getDuration")();
					_aspectRatio = AspectRatio.getAspectRatio(_videoWidth, _videoHeight);
					_originalAspectRatio = _aspectRatio;
					
					callEvents(PlayerEvents.CONNECTION_SUCCESS);
					callEvents(PlayerEvents.MEDIA_INITIALIZED);
					
					resizeAndCenterPlayer();
					
					//Retrieve the volume that user selected last time
					setVolume(_userSettings.getVolume());
				}
				callEvents(PlayerEvents.NOT_BUFFERING);
				
			case 2:
				callEvents(PlayerEvents.NOT_BUFFERING);
				
			case 3:
				callEvents(PlayerEvents.BUFFERING);
				
			case 5:
				callEvents(PlayerEvents.NOT_BUFFERING);
		}
	}
    
	/**
	 * This event is fired whenever the video playback quality changes. For example, if you call the 
	 * setPlaybackQuality(suggestedQuality) function, this event will fire if the playback quality actually 
	 * changes. Your code should respond to the event and should not assume that the quality will automatically 
	 * change when the setPlaybackQuality(suggestedQuality) function is called. Similarly, your code should not 
	 * assume that playback quality will only change as a result of an explicit call to setPlaybackQuality or any 
	 * other function that allows you to set a suggested playback quality.
	 * 
	 * The value that the event broadcasts is the new playback quality. Possible values are "small", "medium", 
	 * "large" and "hd720".
	 * @param	event
	 */
	private function onYoutubePlaybackQualityChange(event:Event):Void
	{
		//trace(Reflect.field(event, "data"));
	}
   
	/**
	 * This event is fired when an error in the player occurs. The possible error codes are 100, 101, 
	 * and 150. The 100 error code is broadcast when the video requested is not found. This occurs when 
	 * a video has been removed (for any reason), or it has been marked as private. The 101 error code is 
	 * broadcast when the video requested does not allow playback in the embedded players. The error code 
	 * 150 is the same as 101, it's just 101 in disguise!
	 * @param	event
	 */
	private function onYoutubeError(event:Event):Void
	{
		trace(Reflect.field(event, "data"));
	}
	//}
	
	
	//{Private Methods
	/**
	 * Function used each time is needed to dispatch an event
	 * @param	type
	 */
	private function callEvents(type:String):Void
	{
		var playerEvent:PlayerEvents = new PlayerEvents(type, true);
		
		playerEvent.aspectRatio = getAspectRatio();
		playerEvent.duration = getDuration();
		playerEvent.fullscreen = isFullscreen();
		playerEvent.mute = getMute();
		playerEvent.volume = getVolume();
		playerEvent.width = _video.width;
		playerEvent.height = _video.height;
		playerEvent.stream = getNetStream();
		playerEvent.sound = getSound();
		playerEvent.time = getCurrentTime();
		playerEvent.id3Info = getId3Info();
		
		dispatchEvent(playerEvent);
	}
	
	/**
	 * Reposition and resizes the video player to fit on screen
	 */
	private function resizeAndCenterPlayer():Void
	{
		if (_streamType != StreamType.YOUTUBE)
		{
			_video.height = _stage.stageHeight;
			_video.width = _video.height * _aspectRatio;
			
			_video.x = (_stage.stageWidth / 2) - (_video.width / 2);
			_video.y = 0;
			
			if (_video.width > _stage.stageWidth && _aspectRatio == _originalAspectRatio)
			{
				var aspectRatio:Float = _videoHeight / _videoWidth;
				_video.width = _stage.stageWidth;
				_video.height = aspectRatio * _video.width;
				_video.x = 0;
				_video.y = (_stage.stageHeight / 2) - (_video.height / 2);
			}
			
			_videoMask.graphics.clear();
			_videoMask.graphics.lineStyle();
			_videoMask.graphics.beginFill(0x000000, 0);
			_videoMask.graphics.drawRect(_video.x, _video.y, _video.width, _video.height);
			_videoMask.graphics.endFill();
		}
		else
		{
			Reflect.field(_youtubeLoader.content, "setSize")(_stage.stageWidth, _stage.stageHeight);
			
			_videoMask.graphics.clear();
			_videoMask.graphics.lineStyle();
			_videoMask.graphics.beginFill(0x000000, 0);
			_videoMask.graphics.drawRect(0, 0, _stage.stageWidth, _stage.stageHeight);
			_videoMask.graphics.endFill();
		}
		
		callEvents(PlayerEvents.RESIZE);
	}
	
	/**
	 * Check the best seek point available if the seekpoints array is available
	 * @param	time time in seconds
	 * @return best seek point in seconds or given one if no seekpoints array is available
	 */
	private function getBestSeekPoint(time:Float):Float
	{
		if (_seekPoints.length > 0)
		{
			var timeOne:String="0";
			var timeTwo:String="0";
			
			for(prop in Reflect.fields(_seekPoints))
			{
				if(Reflect.field(_seekPoints,prop) < time)
				{
					timeOne = prop;
				}
				else
				{
					timeTwo = prop;
				break;
				}
			}

			if(time - _seekPoints[Std.parseInt(timeOne)] < _seekPoints[Std.parseInt(timeTwo)] - time)
			{
				return _seekPoints[Std.parseInt(timeOne)];
			}
			else
			{
				return _seekPoints[Std.parseInt(timeTwo)];
			}
		}
		
		return time;
	}
	
	/**
	 * Checks if the given seek time is already buffered
	 * @param	time time in seconds
	 * @return true if can seek false if not in buffer
	 */
	private function canSeek(time:Float):Bool
	{
		if (_type == InputType.VIDEO)
		{
			time = getBestSeekPoint(time);
		}
		
		var cacheTotal = Math.floor((getDuration() - _startTime) * (getBytesLoaded() / getBytesTotal())) - 1;

		if(time >= _startTime && time < _startTime + cacheTotal)
		{
			return true;
		}
		
		return false;
	}
	//}
	
	
	//{Public methods	
	/**
	 * Loads a video and starts playing it
	 * @param	video video url to load
	 */
	public function load(source:String, type:String="video", streamType:String="file", server:String=""):Void
	{
		stopAndClose();
		
		_type = type;
		_streamType = streamType;
		_mediaSource = source;
		_stopped = false;
		_mediaLoaded = false;
		_firstLoad = true;
		_startTime = 0;
		_downloadCompleted = false;
		_seekPoints = new Array();
		_server = server;
		
		callEvents(PlayerEvents.BUFFERING);
		
		if (_streamType == StreamType.YOUTUBE)
		{
			Security.allowDomain("*");
			Security.allowDomain("www.youtube.com");  
			Security.allowDomain("youtube.com");  
			Security.allowDomain("s.ytimg.com");  
			Security.allowDomain("i.ytimg.com"); 
		
			_youtubeLoader = new Loader();
			_youtubeLoader.contentLoaderInfo.addEventListener(Event.INIT, onYouTubeLoaderInit);
			_youtubeLoader.load(new URLRequest("http://www.youtube.com/apiplayer?version=3"));
		}
		else if (_type == InputType.VIDEO && (_streamType == StreamType.FILE || _streamType == StreamType.PSEUDOSTREAM))
		{	
			_connection.connect(null);
			_stream = new NetStream(_connection);
			_stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
			_stream.bufferTime = _bufferTime;
			_stream.play(source);
			_stream.client = this;
			_video.attachNetStream(_stream);
		}
		else if (_type == InputType.VIDEO && _streamType == StreamType.RTMP)
		{
			_connection.connect(_server);
		}
		else if (_type == InputType.AUDIO && _streamType == StreamType.RTMP)
		{
			_connection.connect(_server);
		}
		else if(_type == InputType.AUDIO && _streamType == StreamType.FILE)
		{
			_sound.load(new URLRequest(source));
		}
	}
	
	/**
	 * Closes the connection and makes player available for another video
	 */
	public function stopAndClose():Void
	{
		if (_mediaLoaded)
		{
			_mediaLoaded = false;
			_isPlaying = false;
			_stopped = true;
			_startTime = 0;
			
			if (_streamType == StreamType.YOUTUBE)
			{
				Reflect.field(_youtubeLoader.content, "destroy")();
			}
			else if (_type == InputType.VIDEO)
			{
				_stream.close();
			}
			else
			{
				_soundChannel.stop();
				_sound.close();
			}
		}
		
		callEvents(PlayerEvents.STOP_CLOSE);
	}
	
	/**
	 * Seeks 8 seconds forward from the current position.
	 * @return current play time after forward
	 */
	public function forward():Float
	{	
		var seekTime = (getCurrentTime() + 8) + _startTime;
		
		if (getDuration() > seekTime)
		{
			seekTime = seek(seekTime);
		}
		
		return seekTime;
	}
	
	/**
	 * Seeks 8 seconds back from the current position.
	 * @return current play time after rewind
	 */
	public function rewind():Float
	{
		var seekTime = (getCurrentTime() - 8) + _startTime;
		
		if (seekTime >= _startTime)
		{
			seekTime = seek(seekTime);
		}
		
		return seekTime;
	}
	
	/**
	 * Seeks video player to a given time in seconds
	 * @param	seekTime time in seconds to seek
	 * @return current play time after seeking
	 */
	public function seek(seekTime:Float):Float
	{
		if (_startTime <= 1 && _downloadCompleted)
		{
			if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
			{
				_stream.seek(seekTime);
			}
			else if (_type == InputType.AUDIO)
			{
				_soundChannel.stop();
				_soundChannel = _sound.play(seekTime * 1000);
				if (!_isPlaying)
				{
					_soundChannel.stop();
				}
				
				setVolume(_userSettings.getVolume());
			}
		}
		else if(_seekPoints.length > 0 && _streamType == StreamType.PSEUDOSTREAM)
		{
			seekTime = getBestSeekPoint(seekTime);
			
			if (canSeek(seekTime))
			{
				_stream.seek(seekTime - _startTime);
			}
			else if(seekTime != _startTime)
			{	
				_startTime = seekTime;
				
				var url:String;
				if (_mediaSource.indexOf("?") != -1)
				{
					url = _mediaSource + "&start=" + seekTime;
				}
				else
				{
					url = _mediaSource + "?start=" + seekTime;
				}
				_stream.play(url);
			}
		}
		else if (_streamType == StreamType.YOUTUBE)
		{
			if (!canSeek(seekTime))
			{
				_startTime = seekTime;
				Reflect.field(_youtubeLoader.content, "seekTo")(seekTime);
			}
			else
			{
				Reflect.field(_youtubeLoader.content, "seekTo")(seekTime);
			}
			
		}
		else if (_streamType == StreamType.RTMP)
		{
			// seekTime = getBestSeekPoint(seekTime); //Not Needed?
			_stream.seek(seekTime);
		}
		else if(canSeek(seekTime))
		{
			if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
			{
				_stream.seek(seekTime);
			}
			else if (_type == InputType.AUDIO)
			{
				_soundChannel.stop();
				_soundChannel = _sound.play(seekTime * 1000);
				if (!_isPlaying)
				{
					_soundChannel.stop();
				}
				
				setVolume(_userSettings.getVolume());
			}
		}
		callEvents(PlayerEvents.SEEK);
		return seekTime;
	}
	
	/**
	 * To check wheter the media is playing
	 * @return true if is playing false otherwise
	 */
	public function isPlaying():Bool
	{
		return _isPlaying;
	}
	
	/**
	 * Cycle betewen aspect ratios
	 * @return new aspect ratio in use
	 */
	public function toggleAspectRatio():Float
	{
		switch(_currentAspectRatio)
		{
			case "original":
				_aspectRatio = AspectRatio._1_1;
				_currentAspectRatio = "1:1";
				
			case "1:1":
				_aspectRatio = AspectRatio._3_2;
				_currentAspectRatio = "3:2";
				
			case "3:2":
				_aspectRatio = AspectRatio._4_3;
				_currentAspectRatio = "4:3";
				
			case "4:3":
				_aspectRatio = AspectRatio._5_4;
				_currentAspectRatio = "5:4";
				
			case "5:4":
				_aspectRatio = AspectRatio._14_9;
				_currentAspectRatio = "14:9";
				
			case "14:9":
				_aspectRatio = AspectRatio._14_10;
				_currentAspectRatio = "14:10";
				
			case "14:10":
				_aspectRatio = AspectRatio._16_9;
				_currentAspectRatio = "16:9";
				
			case "16:9":
				_aspectRatio = AspectRatio._16_10;
				_currentAspectRatio = "16:10";
				
			case "16:10":
				_aspectRatio = _originalAspectRatio;
				_currentAspectRatio = "original";
			
			default:
				_aspectRatio = _originalAspectRatio;
				_currentAspectRatio = "original";
		}
		
		resizeAndCenterPlayer();
		
		callEvents(PlayerEvents.ASPECT_RATIO);
		
		//Store aspect ratio into user settings
		if (_aspectRatio == _originalAspectRatio)
		{
			_userSettings.setAspectRatio(0.0);
		}
		else
		{
			_userSettings.setAspectRatio(_aspectRatio);
		}
		
		return _aspectRatio;
	}
	
	/**
	 * Swithces between play and pause
	 */
	public function togglePlay():Bool
	{
		if (_mediaLoaded)
		{
			if (_mediaEndReached)
			{
				_mediaEndReached = false;
				
				if (_streamType == StreamType.YOUTUBE)
				{
					Reflect.field(_youtubeLoader.content, "seekTo")(0);
					Reflect.field(_youtubeLoader.content, "playVideo")();
				}
				else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
				{
					_stream.seek(0);
					_stream.togglePause();
				}
				else if (_type == InputType.AUDIO)
				{
					_checkAudioTimer.start();
					_soundChannel = _sound.play();
					setVolume(_userSettings.getVolume());
				}
			}
			else if (_mediaLoaded)
			{
				if (_streamType == StreamType.YOUTUBE)
				{
					if (_isPlaying)
					{
						Reflect.field(_youtubeLoader.content, "pauseVideo")();
					}
					else
					{
						Reflect.field(_youtubeLoader.content, "playVideo")();
					}
				}
				else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
				{
					_stream.togglePause();
				}
				else if (_type == InputType.AUDIO)
				{
					if (_isPlaying)
					{
						_soundChannel.stop();
					}
					else
					{
						//If end of audio reached start from beggining
						if (_soundChannel.position + 100 >= _sound.length)
						{
							_soundChannel = _sound.play();
						}
						else 
						{
							_soundChannel = _sound.play(_soundChannel.position);
						}
						
						setVolume(_userSettings.getVolume());
					}
				}
			}
			else if (_stopped)
			{
				load(_mediaSource, _type, _streamType, _server);
			}
			
			_isPlaying = !_isPlaying;
			callEvents(PlayerEvents.PLAY_PAUSE);
			return _isPlaying;
		}
		else if(_mediaSource != "")
		{
			load(_mediaSource, _type, _streamType, _server);
			callEvents(PlayerEvents.BUFFERING);
			return true;
		}
		callEvents(PlayerEvents.PLAY_PAUSE);
		return false;
	}
	
	/**
	 * Switches on or off fullscreen
	 * @return true if fullscreen otherwise false
	 */
	public function toggleFullscreen():Bool 
	{
		if (_fullscreen)
		{
			_stage.displayState = StageDisplayState.NORMAL;
			_stage.focus = _stage;
			return false;
		}
		else
		{	
			if (_useHardWareScaling)
			{
				//Match full screen aspec ratio to desktop
				var aspectRatio = Capabilities.screenResolutionY / Capabilities.screenResolutionX;
				_stage.fullScreenSourceRect = new Rectangle(0, 0, _videoWidth, _videoWidth * aspectRatio);
			}
			else
			{
				//Use desktop resolution
				_stage.fullScreenSourceRect = new Rectangle(0, 0, Capabilities.screenResolutionX ,Capabilities.screenResolutionY);
			}
			
			_stage.displayState = StageDisplayState.FULL_SCREEN;
			_stage.focus = _stage;
			return true;
		}
	}
	
	/**
	 * Toggles betewen high and low quality image rendering
	 * @return true if quality high false otherwise
	 */
	public function toggleQuality():Bool
	{
		if (_videoQualityHigh)
		{
			_video.smoothing = false;
			_video.deblocking = 1;
		}
		else
		{
			_video.smoothing = true;
			_video.deblocking = 5;
		}
		
		_videoQualityHigh = _videoQualityHigh?false:true;
		
		return _videoQualityHigh;
	}
	
	/**
	 * Mutes or unmutes the sound
	 * @return true if muted false if unmuted
	 */
	public function toggleMute():Bool
	{
		var soundTransform:SoundTransform = new SoundTransform();
		var isMute:Bool;
		
		//unmute sound
		if (_soundMuted)
		{
			_soundMuted = false;
			
			if (_volume > 0)
			{
				soundTransform.volume = _volume;
			}
			else
			{
				_volume = 1.0;
				soundTransform.volume = _volume;
			}
			
			isMute =  false;
		}
		
		//mute sound
		else
		{
			_soundMuted = true;
			_volume = _stream.soundTransform.volume;
			soundTransform.volume = 0;
			_stream.soundTransform = soundTransform;
			
			isMute = true;
		}
		
		if (_streamType == StreamType.YOUTUBE)
		{
			Reflect.field(_youtubeLoader.content, "setVolume")(soundTransform.volume * 100);
		}
		else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
		{
			_stream.soundTransform = soundTransform;
		}
		else if (_type == InputType.AUDIO)
		{
			_soundChannel.soundTransform = soundTransform;
			setVolume(_userSettings.getVolume());
		}
		
		callEvents(PlayerEvents.MUTE);
		return isMute;
	}
	
	/**
	 * Check if player is running on fullscreen mode
	 * @return true if fullscreen false if not
	 */
	public function isFullscreen():Bool
	{
		return _stage.displayState == StageDisplayState.FULL_SCREEN;
	}
	
	/**
	 * Raises the volume
	 * @return volume value after raising
	 */
	public function volumeUp():Float
	{
		var soundTransform:SoundTransform = new SoundTransform();
		
		if (_soundMuted)
		{
			_soundMuted = false;
		}
	
		//raise volume if not already at max
		if (_volume < 1)
		{
			if (_streamType == StreamType.YOUTUBE)
			{
				_volume = (Reflect.field(_youtubeLoader.content, "getVolume")() + 10) / 100;
				Reflect.field(_youtubeLoader.content, "setVolume")(_volume * 100);
			}
			else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
			{
				_volume = _stream.soundTransform.volume + (10/100);
				soundTransform.volume = _volume;
				_stream.soundTransform = soundTransform;
			}
			else if (_type == InputType.AUDIO)
			{
				_volume = _soundChannel.soundTransform.volume + (10/100);
				soundTransform.volume = _volume;
				_soundChannel.soundTransform = soundTransform;
			}
		}
		
		//reset volume to 1.0 if already reached max
		if (_volume >= 1)
		{
			_volume = 1.0;
		}
		
		//Store volume into user settings
		_userSettings.setVolume(_volume);
		
		callEvents(PlayerEvents.VOLUME_UP);
		return _volume;
	}
	
	/**
	 * Lower the volume
	 * @return volume value after lowering
	 */
	public function volumeDown():Float
	{
		var soundTransform:SoundTransform = new SoundTransform();
		
		//lower sound
		if(!_soundMuted)
		{	
			if (_streamType == StreamType.YOUTUBE)
			{
				_volume = (Reflect.field(_youtubeLoader.content, "getVolume")() - 10) / 100;
				Reflect.field(_youtubeLoader.content, "setVolume")(_volume * 100);
			}
			else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
			{
				_volume = _stream.soundTransform.volume - (10/100);
				soundTransform.volume = _volume;
				_stream.soundTransform = soundTransform;
			}
			else if (_type == InputType.AUDIO)
			{
				_volume = _soundChannel.soundTransform.volume - (10/100);
				soundTransform.volume = _volume;
				_soundChannel.soundTransform = soundTransform;
			}
			
			//if volume reached min is muted
			if (_volume <= 0)
			{
				_soundMuted = true;
				_volume = 0;
			}
		}
		
		//Store volume into user settings
		_userSettings.setVolume(_volume);
		
		callEvents(PlayerEvents.VOLUME_DOWN);
		return _volume;
	}
	//}
	
	
	//{Setters
	/**
	 * Set input type
	 * @param	type Allowable values are audio, video
	 */
	public function setType(type:String):Void
	{
		_type = type;
	}
	
	/**
	 * Set streaming type
	 * @param	streamType Allowable values are file, http, rmtp
	 */
	public function setStreamType(streamType:String):Void
	{
		_streamType = streamType;
	}
	
	/**
	 * Sets the server url for rtmp streams
	 * @param	server
	 */
	public function setServer(server:String):Void
	{
		_server = server;
	}
	
	/**
	 * To set the video source in case we dont want to start downloading at first so when use tooglePlay the
	 * media is loaded automatically
	 * @param	source
	 */
	public function setSource(source):Void
	{
		_mediaSource = source;
	}
	
	/**
	 * Changes the current volume
	 * @param	volume
	 */
	public function setVolume(volume:Float):Void
	{
		var soundTransform:SoundTransform = new SoundTransform();
		
		if (volume > _volume) {
			callEvents(PlayerEvents.VOLUME_UP);
		}

		if (volume < _volume) {
			callEvents(PlayerEvents.VOLUME_DOWN);
		}

		if (volume > 0)
		{
			_soundMuted = false;
			_volume = volume;
		}
		else
		{
			_soundMuted = true;
			_volume = 1.0;
		}
		
		soundTransform.volume = volume;
		
		if (!_firstLoad) //To prevent errors if objects aren't initialized
		{
			if (_streamType == StreamType.YOUTUBE)
			{
				Reflect.field(_youtubeLoader.content, "setVolume")(soundTransform.volume * 100);
			}
			else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
			{
				_stream.soundTransform = soundTransform;
			}
			else if (_type == InputType.AUDIO)
			{
				_soundChannel.soundTransform = soundTransform;
			}
		}
		
		//Store volume into user settings
		_userSettings.setVolume(_volume);

		callEvents(PlayerEvents.VOLUME_CHANGE);
	}
	
	/**
	 * Changes the buffer time for local and pseudo streaming
	 * @param	time in seconds
	 */
	public function setBufferTime(time:Float):Void
	{
		if (time > 0)
		{
			_bufferTime = time;
		}
	}
	
	/**
	 * Changes the aspec ratio of current playing media and resizes video player
	 * @param	aspectRatio new aspect ratio value
	 */
	public function setAspectRatio(aspectRatio:Float):Void
	{
		_aspectRatio = aspectRatio;
		
		switch(_aspectRatio)
		{
			case 0.0:
				_currentAspectRatio = "original";
				
			case AspectRatio._1_1:
				_currentAspectRatio = "1:1";
				
			case AspectRatio._3_2:
				_currentAspectRatio = "3:2";
				
			case AspectRatio._4_3:
				_currentAspectRatio = "4:3";
				
			case AspectRatio._5_4:
				_currentAspectRatio = "5:4";
				
			case AspectRatio._14_9:
				_currentAspectRatio = "14:9";
				
			case AspectRatio._14_10:
				_currentAspectRatio = "14:10";
				
			case AspectRatio._16_9:
				_currentAspectRatio = "16:9";
				
			case AspectRatio._16_10:
				_currentAspectRatio = "16:10";
		}
		
		resizeAndCenterPlayer();
		
		//Store aspect ratio into user settings
		_userSettings.setAspectRatio(_aspectRatio);
	}
	
	/**
	 * Enable or disable hardware scaling
	 * @param	value true to enable false to disable
	 */
	public function setHardwareScaling(value:Bool):Void
	{
		_useHardWareScaling = value;
	}
	//}
	
	
	//{Getters
	/**
	 * Gets the volume amount 0.0 to 1.0
	 * @return 
	 */
	public function getVolume():Float
	{
		return _volume;
	}
	
	/**
	 * The current aspect ratio of the loaded Player
	 * @return
	 */
	public function getAspectRatio():Float
	{
		return _aspectRatio;
	}
	
	/**
	 * The current aspect ratio of the loaded Player in string format
	 * @return
	 */
	public function getAspectRatioString():String
	{
		return _currentAspectRatio;
	}
	
	/**
	 * Original aspect ratio of the video
	 * @return original aspect ratio
	 */
	public function getOriginalAspectRatio():Float
	{
		return _originalAspectRatio;
	}
	
	/**
	 * Total duration time of the loaded media
	 * @return time in seconds
	 */
	public function getDuration():Float
	{
		return _mediaDuration;
	}
	
	/**
	 * The time in seconds where the player started downloading
	 * @return time in seconds
	 */
	public function getStartTime():Float
	{
		return _startTime;
	}
	
	/**
	 * The stream associated with the player
	 * @return netstream object
	 */
	public function getNetStream():NetStream
	{
		return _stream;
	}
	
	/**
	 * Video object associated to the player
	 * @return video object for further manipulation
	 */
	public function getVideo():Video
	{	
		return _video;
	}
	
	/**
	 * Sound object associated to the player
	 * @return sound object for further manipulation
	 */
	public function getSound():Sound
	{
		return _sound;
	}
	
	/**
	 * The id3 info of sound object
	 * @return
	 */
	public function getId3Info():ID3Info
	{
		return _id3Info;
	}
	
	/**
	 * The current sound state
	 * @return true if mute otherwise false
	 */
	public function getMute():Bool
	{
		return _soundMuted;
	}
	
	/**
	 * The amount of total bytes
	 * @return amount of bytes
	 */
	public function getBytesTotal():Float
	{
		var bytesTotal:Float = 0;
		
		if (_streamType == StreamType.YOUTUBE)
		{
			if(_youtubeLoader != null && _mediaLoaded)
			bytesTotal = Reflect.field(_youtubeLoader.content, "getVideoBytesTotal")();
		}
		else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
		{
			bytesTotal = _stream.bytesTotal;
		}
		else if (_type == InputType.AUDIO)
		{
			bytesTotal = _sound.bytesTotal;
		}
		
		return bytesTotal;
	}
	
	/**
	 * The amount of bytes loaded
	 * @return amount of bytes
	 */
	public function getBytesLoaded():Float
	{
		var bytesLoaded:Float = 0;
		
		if (_streamType == StreamType.YOUTUBE)
		{
			if(_youtubeLoader != null && _mediaLoaded)
			bytesLoaded = Reflect.field(_youtubeLoader.content, "getVideoBytesLoaded")();
		}
		else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
		{
			bytesLoaded = _stream.bytesLoaded;
		}
		else if (_type == InputType.AUDIO)
		{
			bytesLoaded = _sound.bytesLoaded;
		}
		
		return bytesLoaded;
	}
	
	/**
	 * Current playing file type
	 * @return audio or video
	 */
	public function getType():String
	{
		return _type;
	}
	
	/**
	 * The stream method for the current playing media
	 * @return
	 */
	public function getStreamType():String
	{
		return _streamType;
	}
	
	/**
	 * The server url for current rtmp stream
	 * @return
	 */
	public function getServer():String
	{
		return _server;
	}
	
	/**
	 * To check current quality mode
	 * @return true if high quality false if low
	 */
	public function getQuality():Bool
	{
		return _videoQualityHigh;
	}
	
	/**
	 * The current playing time
	 * @return current playing time in seconds
	 */
	public function getCurrentTime():Float
	{
		var time:Float = 0;
		if (_streamType == StreamType.YOUTUBE)
		{
			if(_youtubeLoader != null)
			{
				time = Reflect.field(_youtubeLoader.content, "getCurrentTime")();
			}
			else
			{
				time = 0;
			}
		}
		else if (_streamType == StreamType.PSEUDOSTREAM)
		{
			time = getStartTime() + _stream.time;
		}
		else if (_type == InputType.VIDEO || _streamType == StreamType.RTMP)
		{
			time = _stream.time;
		}
		else if (_type == InputType.AUDIO)
		{
			if(_soundChannel != null)
			{	
				time = _soundChannel.position / 1000;
			}
			else
			{
				time = 0;
			}
		}
		
		return time;
	}
	//}
}