/*
 * License
 */
package org.igniterealtime.xiff.core
{
	import flash.events.TimerEvent;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.igniterealtime.xiff.events.*;
	import org.igniterealtime.xiff.logging.LoggerFactory;
	import org.igniterealtime.xiff.util.Callback;

	/**
	 * Bidirectional-streams Over Synchronous HTTP (BOSH)
	 * @see http://xmpp.org/extensions/xep-0206.html
	 */
	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static const BOSH_VERSION:String = "1.6";

		private static const HTTPS_PORT:int = 7443;

		private static const HTTP_PORT:int = 7070;

		private static const headers:Object = {
			"post": [],
			"get": [ 'Cache-Control',
					 'no-store',
					 'Cache-Control',
					 'no-cache',
					 'Pragma', 'no-cache' ]
		};

		private static const logger:ILogger = LoggerFactory.getLogger( "org.igniterealtime.xiff.core.XMPPBOSHConnection" );

		private var _boshPath:String = "http-bind/";

		private var _hold:uint = 1;

		private var _maxConcurrentRequests:uint = 2;

		private var _port:Number;

		private var _secure:Boolean;

		private var _wait:uint = 20;

		private var boshPollingInterval:uint = 10000;

		private var inactivity:uint;

		private var isDisconnecting:Boolean = false;

		private var lastPollTime:Date = null;

		private var maxPause:uint;

		private var pauseEnabled:Boolean = false;

		private var pauseTimer:Timer;

		private var pollingEnabled:Boolean = false;

		private var requestCount:int = 0;

		private var requestQueue:Array = [];

		private var responseQueue:Array = [];

		private const responseTimer:Timer = new Timer( 0.0, 1 );

		private var rid:Number;

		private var sid:String;

		private var streamRestarted:Boolean;

		/**
		 *
		 * @param	secure
		 */
		public function XMPPBOSHConnection( secure:Boolean = false ):void
		{
			super();
			this.secure = secure;
		}

		override public function connect( streamType:String = null ):Boolean
		{
			logger.debug( "BOSH connect()" );

			var attrs:Object = {
				"xml:lang": "en",
				"xmlns": "http://jabber.org/protocol/httpbind",
				"xmlns:xmpp": "urn:xmpp:xbosh",
				"xmpp:version": "1.0",
				"hold": hold,
				"rid": nextRID,
				"secure": secure,
				"wait": wait,
				"ver": BOSH_VERSION,
				"to": domain
			};

			var result:XMLNode = new XMLNode( 1, "body" );
			result.attributes = attrs;
			sendRequests( result );

			return true;
		}

		override public function disconnect():void
		{
			if ( active )
			{
				var data:XMLNode = createRequest();
				data.attributes.type = "terminate";
				sendRequests( data );
				active = false;
				loggedIn = false;
				dispatchEvent( new DisconnectionEvent());
			}
		}

		/**
		 * @return	true if pause request is sent
		 */
		public function pauseSession( seconds:uint ):Boolean
		{
			logger.debug( "Pausing session for {0} seconds", seconds );

			var pauseDuration:uint = seconds * 1000;
			if ( !pauseEnabled || pauseDuration > maxPause || pauseDuration <= boshPollingInterval )
				return false;

			pollingEnabled = false;

			var data:XMLNode = createRequest();
			data.attributes[ "pause" ] = seconds;
			sendRequests( data );

			pauseTimer = new Timer( pauseDuration - 2000, 1 );
			pauseTimer.addEventListener( TimerEvent.TIMER, handlePauseTimeout );
			pauseTimer.start();

			return true;
		}

		public function processConnectionResponse( responseBody:XMLNode ):void
		{
			dispatchEvent( new ConnectionSuccessEvent());

			var attributes:Object = responseBody.attributes;

			sid = attributes.sid;
			wait = attributes.wait;

			if ( attributes.polling )
			{
				boshPollingInterval = attributes.polling * 1000;
			}
			if ( attributes.inactivity )
			{
				inactivity = attributes.inactivity * 1000;
			}
			if ( attributes.maxpause )
			{
				maxPause = attributes.maxpause * 1000;
				pauseEnabled = true;
			}
			if ( attributes.requests )
			{
				maxConcurrentRequests = attributes.requests;
			}

			logger.debug( "Polling interval: {0}", boshPollingInterval );
			logger.debug( "Inactivity timeout: {0}", inactivity );
			logger.debug( "Max requests: {0}", maxConcurrentRequests );
			logger.debug( "Max pause: {0}", maxPause );

			active = true;

			addEventListener( LoginEvent.LOGIN, handleLogin );
			responseTimer.addEventListener( TimerEvent.TIMER_COMPLETE, processResponse );
		}

		//do nothing, we use polling instead
		override public function sendKeepAlive():void
		{
		}

		override protected function restartStream():void
		{
			var data:XMLNode = createRequest();
			data.attributes[ "xmpp:restart" ] = "true";
			data.attributes[ "xmlns:xmpp" ] = "urn:xmpp:xbosh";
			data.attributes[ "xml:lang" ] = "en";
			data.attributes[ "to" ] = domain;
			sendRequests( data );
			streamRestarted = true;
		}

		override protected function sendXML( body:* ):void
		{
			sendQueuedRequests( body );
		}

		private function createRequest( bodyContent:Array = null ):XMLNode
		{
			var attrs:Object = {
				"xmlns": "http://jabber.org/protocol/httpbind",
				"rid": nextRID,
				"sid": sid
			};
			var req:XMLNode = new XMLNode( 1, "body" );
			if ( bodyContent )
			{
				for each ( var content:XMLNode in bodyContent )
				{
					req.appendChild( content );
				}
			}

			req.attributes = attrs;

			return req;
		}

		private function handleLogin( e:LoginEvent ):void
		{
			pollingEnabled = true;
			pollServer();
		}

		private function handlePauseTimeout( event:TimerEvent ):void
		{
			logger.debug( "handlePauseTimeout" );
			pollingEnabled = true;
			pollServer();
		}

		private function httpError( req:XMLNode, isPollResponse:Boolean, event:FaultEvent ):void
		{
			disconnect();
			dispatchError( "Unknown HTTP Error", event.fault.rootCause.text, "",
						   -1 );
		}

		private function httpResponse( req:XMLNode, isPollResponse:Boolean, event:ResultEvent ):void
		{
			requestCount--;
			var rawXML:String = event.result as String;

			logger.info( "INCOMING {0}", rawXML );

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(xmlData.toString());

			var incomingEvent:IncomingDataEvent = new IncomingDataEvent();
			incomingEvent.data = byteData;
			dispatchEvent( incomingEvent );

			if ( streamRestarted && !bodyNode.hasChildNodes())
			{
				streamRestarted = false;
				bindConnection();
			}

			if ( bodyNode.attributes[ "type" ] == "terminate" )
			{
				dispatchError( "BOSH Error", bodyNode.attributes[ "condition" ],
							   "", -1 );
				active = false;
			}

			if ( bodyNode.attributes[ "sid" ] && !loggedIn )
			{
				processConnectionResponse( bodyNode );

				var featuresFound:Boolean = false;
				for each ( var child:XMLNode in bodyNode.childNodes )
				{
					if ( child.nodeName == "stream:features" )
						featuresFound = true;
				}
				if ( !featuresFound )
				{
					pollingEnabled = true;
					pollServer();
				}
			}

			for each ( var childNode:XMLNode in bodyNode.childNodes )
			{
				responseQueue.push( childNode );
			}

			resetResponseProcessor();

			//if we have no outstanding requests, then we're free to send a poll at the next opportunity
			if ( requestCount == 0 && !sendQueuedRequests())
				pollServer();
		}

		private function pollServer():void
		{
			//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
			if ( !isActive() || !pollingEnabled || sendQueuedRequests() || requestCount >
				0 )
				return;

			//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
			sendRequests( null, true );
		}

		private function processResponse( event:TimerEvent = null ):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch ( nodeName )
			{
				case "stream:features":
					handleStreamFeatures( currentNode );
					streamRestarted = false; //avoid triggering the old server workaround
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;

				case "failure":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify",
								   500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if ( responseQueue.length > 0 )
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function sendQueuedRequests( body:* = null ):Boolean
		{
			if ( body )
			{
				requestQueue.push( body );
			}
			else if ( requestQueue.length == 0 )
			{
				return false;
			}

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests( data:XMLNode = null, isPoll:Boolean = false ):Boolean
		{
			if ( requestCount >= maxConcurrentRequests )
			{
				return false;
			}

			requestCount++;

			if ( !data )
			{
				if ( isPoll )
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for ( var i:uint = 0; i < 10 && requestQueue.length > 0; ++i )
					{
						temp.push( requestQueue.shift());
					}
					data = createRequest( temp );
				}
			}

			// TODO: Could this be replaced with URLLoader ?
			//build the http request
			var request:HTTPService = new HTTPService();
			request.method = "post";
			request.headers = headers[ request.method ];
			request.url = httpServer;
			request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			request.contentType = "text/xml";

			var responseCallback:Callback = new Callback( this, httpResponse, data,
														  isPoll );
			var errorCallback:Callback = new Callback( this, httpError, data, isPoll );

			request.addEventListener( ResultEvent.RESULT, responseCallback.call,
									  false );
			request.addEventListener( FaultEvent.FAULT, errorCallback.call, false );

			request.send( data );

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(data.toString());
			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = byteData;
			dispatchEvent( event );

			if ( isPoll )
			{
				lastPollTime = new Date();
				logger.info( "Polling" );
			}

			logger.info( "OUTGOING {0}", data );

			return true;
		}

		public function get boshPath():String
		{
			return _boshPath;
		}
		public function set boshPath( value:String ):void
		{
			_boshPath = value;
		}

		private function get nextRID():Number
		{
			if ( !rid )
				rid = Math.floor( Math.random() * 1000000 );
			return ++rid;
		}

		public function get wait():uint
		{
			return _wait;
		}
		public function set wait( value:uint ):void
		{
			_wait = value;
		}

		public function get secure():Boolean
		{
			return _secure;
		}
		public function set secure( flag:Boolean ):void
		{
			logger.debug( "set secure: {0}", flag );
			_secure = flag;
			port = _secure ? HTTPS_PORT : HTTP_PORT;
		}

		override public function get port():Number
		{
			return _port;
		}
		override public function set port( portnum:Number ):void
		{
			logger.debug( "set port: {0}", portnum );
			_port = portnum;
		}

		public function get hold():uint
		{
			return _hold;
		}
		public function set hold( value:uint ):void
		{
			_hold = value;
		}

		public function get httpServer():String
		{
			return ( secure ? "https" : "http" ) + "://" + server + ":" + port +
				"/" + boshPath;
		}

		public function get maxConcurrentRequests():uint
		{
			return _maxConcurrentRequests;
		}
		public function set maxConcurrentRequests( value:uint ):void
		{
			_maxConcurrentRequests = value;
		}
	}
}

/*
 * License
 */
package org.igniterealtime.xiff.core
{
	import flash.events.*;
	import flash.net.*;
	import flash.utils.ByteArray;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.logging.ILogger;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.igniterealtime.xiff.events.*;
	import org.igniterealtime.xiff.logging.LoggerFactory;
	import org.igniterealtime.xiff.util.Callback;

	/**
	 * Bidirectional-streams Over Synchronous HTTP (BOSH)
	 * @see http://xmpp.org/extensions/xep-0206.html
	 */
	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static const BOSH_VERSION:String = "1.6";

		private static const HTTPS_PORT:int = 7443;

		private static const HTTP_PORT:int = 7070;

		/**
		 * Keys should match URLRequestMethod constants.
		 */
		private static const headers:Object = {
			"post": [],
			"get": [ 'Cache-Control',
					 'no-store',
					 'Cache-Control',
					 'no-cache',
					 'Pragma', 'no-cache' ]
		};

		private static const logger:ILogger = LoggerFactory.getLogger( "org.igniterealtime.xiff.core.XMPPBOSHConnection" );

		private var _boshPath:String = "http-bind/";

		private var _hold:uint = 1;

		private var _maxConcurrentRequests:uint = 2;

		private var _port:Number;

		private var _secure:Boolean;

		private var _wait:uint = 20;

		private var boshPollingInterval:uint = 10000;

		private var inactivity:uint;

		private var isDisconnecting:Boolean = false;

		private var lastPollTime:Date = null;

		private var maxPause:uint;

		private var pauseEnabled:Boolean = false;

		private var pauseTimer:Timer;

		private var pollingEnabled:Boolean = false;

		private var requestCount:int = 0;

		private var requestQueue:Array = [];

		private var responseQueue:Array = [];

		private var responseTimer:Timer;

		private var rid:Number;

		private var sid:String;

		private var streamRestarted:Boolean;

		/**
		 *
		 * @param	secure
		 */
		public function XMPPBOSHConnection( secure:Boolean = false ):void
		{
			super();
			this.secure = secure;
			responseTimer = new Timer( 0.0, 1 );
		}

		override public function connect( streamType:String = null ):Boolean
		{
			logger.debug( "BOSH connect()" );

			var attrs:Object = {
				"xml:lang": "en",
				"xmlns": "http://jabber.org/protocol/httpbind",
				"xmlns:xmpp": "urn:xmpp:xbosh",
				"xmpp:version": "1.0",
				"hold": hold,
				"rid": nextRID,
				"secure": secure,
				"wait": wait,
				"ver": BOSH_VERSION,
				"to": domain
			};

			var result:XMLNode = new XMLNode( 1, "body" );
			result.attributes = attrs;
			sendRequests( result );

			return true;
		}

		override public function disconnect():void
		{
			if ( active )
			{
				var data:XMLNode = createRequest();
				data.attributes.type = "terminate";
				sendRequests( data );
				active = false;
				loggedIn = false;
				dispatchEvent( new DisconnectionEvent());
			}
		}

		/**
		 * @return	true if pause request is sent
		 */
		public function pauseSession( seconds:uint ):Boolean
		{
			logger.debug( "Pausing session for {0} seconds", seconds );

			var pauseDuration:uint = seconds * 1000;
			if ( !pauseEnabled || pauseDuration > maxPause || pauseDuration <= boshPollingInterval )
				return false;

			pollingEnabled = false;

			var data:XMLNode = createRequest();
			data.attributes[ "pause" ] = seconds;
			sendRequests( data );

			pauseTimer = new Timer( pauseDuration - 2000, 1 );
			pauseTimer.addEventListener( TimerEvent.TIMER, handlePauseTimeout );
			pauseTimer.start();

			return true;
		}

		public function processConnectionResponse( responseBody:XMLNode ):void
		{
			dispatchEvent( new ConnectionSuccessEvent());

			var attributes:Object = responseBody.attributes;

			sid = attributes.sid;
			wait = attributes.wait;

			if ( attributes.polling )
			{
				boshPollingInterval = attributes.polling * 1000;
			}
			if ( attributes.inactivity )
			{
				inactivity = attributes.inactivity * 1000;
			}
			if ( attributes.maxpause )
			{
				maxPause = attributes.maxpause * 1000;
				pauseEnabled = true;
			}
			if ( attributes.requests )
			{
				maxConcurrentRequests = attributes.requests;
			}

			logger.debug( "Polling interval: {0}", boshPollingInterval );
			logger.debug( "Inactivity timeout: {0}", inactivity );
			logger.debug( "Max requests: {0}", maxConcurrentRequests );
			logger.debug( "Max pause: {0}", maxPause );

			active = true;

			addEventListener( LoginEvent.LOGIN, handleLogin );
			responseTimer.addEventListener( TimerEvent.TIMER_COMPLETE, processResponse );
		}

		//do nothing, we use polling instead
		override public function sendKeepAlive():void
		{
		}

		override protected function restartStream():void
		{
			var data:XMLNode = createRequest();
			data.attributes[ "xmpp:restart" ] = "true";
			data.attributes[ "xmlns:xmpp" ] = "urn:xmpp:xbosh";
			data.attributes[ "xml:lang" ] = "en";
			data.attributes[ "to" ] = domain;
			sendRequests( data );
			streamRestarted = true;
		}

		override protected function sendXML( body:* ):void
		{
			sendQueuedRequests( body );
		}

		private function createRequest( bodyContent:Array = null ):XMLNode
		{
			var attrs:Object = {
				"xmlns": "http://jabber.org/protocol/httpbind",
				"rid": nextRID,
				"sid": sid
			};
			var req:XMLNode = new XMLNode( 1, "body" );
			if ( bodyContent )
			{
				for each ( var content:XMLNode in bodyContent )
				{
					req.appendChild( content );
				}
			}

			req.attributes = attrs;

			return req;
		}

		private function handleLogin( e:LoginEvent ):void
		{
			pollingEnabled = true;
			pollServer();
		}

		private function handlePauseTimeout( event:TimerEvent ):void
		{
			logger.debug( "handlePauseTimeout" );
			pollingEnabled = true;
			pollServer();
		}

		private function httpError( req:XMLNode, isPollResponse:Boolean, event:FaultEvent ):void
		{
			disconnect();
			dispatchError( "Unknown HTTP Error", event.fault.rootCause.text, "",
						   -1 );
		}

		private function httpResponse( req:XMLNode, isPollResponse:Boolean, event:ResultEvent ):void
		{
			requestCount--;
			var rawXML:String = event.result as String;

			logger.info( "INCOMING {0}", rawXML );

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(xmlData.toString());

			var incomingEvent:IncomingDataEvent = new IncomingDataEvent();
			incomingEvent.data = byteData;
			dispatchEvent( incomingEvent );

			if ( streamRestarted && !bodyNode.hasChildNodes())
			{
				streamRestarted = false;
				bindConnection();
			}

			if ( bodyNode.attributes[ "type" ] == "terminate" )
			{
				dispatchError( "BOSH Error", bodyNode.attributes[ "condition" ],
							   "", -1 );
				active = false;
			}

			if ( bodyNode.attributes[ "sid" ] && !loggedIn )
			{
				processConnectionResponse( bodyNode );

				var featuresFound:Boolean = false;
				for each ( var child:XMLNode in bodyNode.childNodes )
				{
					if ( child.nodeName == "stream:features" )
						featuresFound = true;
				}
				if ( !featuresFound )
				{
					pollingEnabled = true;
					pollServer();
				}
			}

			for each ( var childNode:XMLNode in bodyNode.childNodes )
			{
				responseQueue.push( childNode );
			}

			resetResponseProcessor();

			//if we have no outstanding requests, then we're free to send a poll at the next opportunity
			if ( requestCount == 0 && !sendQueuedRequests())
				pollServer();
		}

		private function pollServer():void
		{
			//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
			if ( !isActive() || !pollingEnabled || sendQueuedRequests() || requestCount >
				0 )
				return;

			//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
			sendRequests( null, true );
		}

		private function processResponse( event:TimerEvent = null ):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch ( nodeName )
			{
				case "stream:features":
					handleStreamFeatures( currentNode );
					streamRestarted = false; //avoid triggering the old server workaround
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;

				case "failure":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify",
								   500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if ( responseQueue.length > 0 )
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function sendQueuedRequests( body:* = null ):Boolean
		{
			if ( body )
			{
				requestQueue.push( body );
			}
			else if ( requestQueue.length == 0 )
			{
				return false;
			}

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests( data:XMLNode = null, isPoll:Boolean = false ):Boolean
		{
			if ( requestCount >= maxConcurrentRequests )
			{
				return false;
			}

			requestCount++;

			if ( !data )
			{
				if ( isPoll )
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for ( var i:uint = 0; i < 10 && requestQueue.length > 0; ++i )
					{
						temp.push( requestQueue.shift());
					}
					data = createRequest( temp );
				}
			}
			/*
			var req:URLRequest = new URLRequest(httpServer);
			req.method = URLRequestMethod.POST;
			req.contentType = "text/xml";
			req.requestHeaders = headers[ req.method ];
			req.data = data;
			var loader:URLLoader = new URLLoader();
			loader.dataFormat = URLLoaderDataFormat.TEXT;
			loader.addEventListener(Event.COMPLETE, onRequestComplete);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socketSecurityError);
			loader.addEventListener(IOErrorEvent.IO_ERROR, socketIOError);
			loader.load(req);
			*/

			// TODO: Could this be replaced with URLLoader ?
			//build the http request
			var request:HTTPService = new HTTPService();
			request.method = "post";
			request.headers = headers[ request.method ];
			request.url = httpServer;
			request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			request.contentType = "text/xml";

			var responseCallback:Callback = new Callback( this, httpResponse, data,
														  isPoll );
			var errorCallback:Callback = new Callback( this, httpError, data, isPoll );

			request.addEventListener( ResultEvent.RESULT, responseCallback.call,
									  false );
			request.addEventListener( FaultEvent.FAULT, errorCallback.call, false );

			request.send( data );

			var byteData:ByteArray = new ByteArray();
			byteData.writeUTFBytes(data.toString());
			var event:OutgoingDataEvent = new OutgoingDataEvent();
			event.data = byteData;
			dispatchEvent( event );

			if ( isPoll )
			{
				lastPollTime = new Date();
				logger.info( "Polling" );
			}

			logger.info( "OUTGOING {0}", data );

			return true;
		}

		public function get boshPath():String
		{
			return _boshPath;
		}
		public function set boshPath( value:String ):void
		{
			_boshPath = value;
		}

		private function get nextRID():Number
		{
			if ( !rid )
				rid = Math.floor( Math.random() * 1000000 );
			return ++rid;
		}

		public function get wait():uint
		{
			return _wait;
		}
		public function set wait( value:uint ):void
		{
			_wait = value;
		}

		public function get secure():Boolean
		{
			return _secure;
		}
		public function set secure( flag:Boolean ):void
		{
			logger.debug( "set secure: {0}", flag );
			_secure = flag;
			port = _secure ? HTTPS_PORT : HTTP_PORT;
		}

		override public function get port():Number
		{
			return _port;
		}
		override public function set port( portnum:Number ):void
		{
			logger.debug( "set port: {0}", portnum );
			_port = portnum;
		}

		public function get hold():uint
		{
			return _hold;
		}
		public function set hold( value:uint ):void
		{
			_hold = value;
		}

		public function get httpServer():String
		{
			return ( secure ? "https" : "http" ) + "://" + server + ":" + port +
				"/" + boshPath;
		}

		public function get maxConcurrentRequests():uint
		{
			return _maxConcurrentRequests;
		}
		public function set maxConcurrentRequests( value:uint ):void
		{
			_maxConcurrentRequests = value;
		}
	}
}

Rewrite work in progress...

Rewrite work in progress...

git-svn-id: c197267f952b24206666de142881703007ca05d5@11143 b35dd754-fafc-0310-a699-88a17e54d16e


package aerys.minko.scene.controller
{
	import aerys.minko.render.Viewport;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.KeyboardManager;
	import aerys.minko.type.MouseManager;

	import flash.display.BitmapData;
	import flash.utils.Dictionary;

	public class ScriptController extends EnterFrameController
	{
        private var _scene          : Scene;
        private var _started        : Dictionary;
		private var _lastTime		: Number;
		private var _deltaTime		: Number;
		private var _currentTarget	: ISceneNode;
		private var _viewport		: Viewport;

        protected function get scene() : Scene
        {
            return _scene;
        }

		protected function get deltaTime() : Number
		{
			return _deltaTime;
		}

		protected function get keyboard() : KeyboardManager
		{
			return _viewport.keyboardManager;
		}

		protected function get mouse() : MouseManager
		{
			return _viewport.mouseManager;
		}

		protected function get viewport() : Viewport
		{
			return _viewport;
		}

		public function ScriptController(targetType	: Class = null)
		{
			super(targetType);
		}

        protected function initialize() : void
        {
            _started = new Dictionary(true);
        }

        override protected function targetAddedToSceneHandler(target    : ISceneNode,
                                                              scene     : Scene) : void
        {
            if (_scene && scene != _scene)
                throw new Error(
                    'The same ScriptController instance can not be used in more than one scene ' +
                    'at a time.'
                );

            _scene = scene;
        }

        override protected function targetRemovedFromSceneHandler(target    : ISceneNode,
                                                                  scene     : Scene) : void
        {
            if (numTargetsInScene == 0)
                _scene = null;
        }

		override protected function sceneEnterFrameHandler(scene		: Scene,
														   viewport		: Viewport,
														   destination	: BitmapData,
														   time			: Number) : void
		{
			_viewport = viewport;
			_deltaTime = _lastTime - time;
			_lastTime = time;

            beforeUpdate();

			var numTargets : uint = this.numTargets;

			for (var i : uint = 0; i < numTargets; ++i)
            {
                var target : ISceneNode = getTarget(i);

                if (target.scene)
                {
                    if (!_started[target])
                    {
                        _started[target] = true;
                        start(target);
                    }

    				update(target);
                }
                else if (_started[target])
                {
                    _started[target] = false;
                    stop(target);
                }
            }

            afterUpdate();
		}

        /**
         * The 'start' method is called on a script target at the first frame occuring after it
         * has been added to the scene.
         *
         * @param target
         *
         */
		protected function start(target : ISceneNode) : void
		{
			// nothing
		}

        /**
         * The 'beforeUpdate' method is called before each target is updated via the 'update'
         * method.
         *
         */
        protected function beforeUpdate() : void
        {
            // nothing
        }

		protected function update(target : ISceneNode) : void
		{
			// nothing
		}

        /**
         * The 'afterUpdate' method is called after each target has been updated via the 'update'
         * method.
         *
         */
        protected function afterUpdate() : void
        {
            // nothing
        }

        /**
         * The 'start' method is called on a script target at the first frame occuring after it
         * has been removed from the scene.
         *
         * @param target
         *
         */
		protected function stop(target : ISceneNode) : void
		{
			// nothing
		}
	}
}

package aerys.minko.scene.controller
{
	import aerys.minko.render.Viewport;
	import aerys.minko.scene.node.ISceneNode;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.KeyboardManager;
	import aerys.minko.type.MouseManager;

	import flash.display.BitmapData;
	import flash.utils.Dictionary;

	public class ScriptController extends EnterFrameController
	{
        private var _scene          : Scene;
        private var _started        : Dictionary;
		private var _lastTime		: Number;
		private var _deltaTime		: Number;
		private var _currentTarget	: ISceneNode;
		private var _viewport		: Viewport;

        protected function get scene() : Scene
        {
            return _scene;
        }

		protected function get deltaTime() : Number
		{
			return _deltaTime;
		}

		protected function get keyboard() : KeyboardManager
		{
			return _viewport.keyboardManager;
		}

		protected function get mouse() : MouseManager
		{
			return _viewport.mouseManager;
		}

		protected function get viewport() : Viewport
		{
			return _viewport;
		}

		public function ScriptController(targetType	: Class = null)
		{
			super(targetType);

            initialize();
		}

        protected function initialize() : void
        {
            _started = new Dictionary(true);
        }

        override protected function targetAddedToSceneHandler(target    : ISceneNode,
                                                              scene     : Scene) : void
        {
            super.targetAddedToSceneHandler(target, scene);

            if (_scene && scene != _scene)
                throw new Error(
                    'The same ScriptController instance can not be used in more than one scene ' +
                    'at a time.'
                );

            _scene = scene;
        }

        override protected function targetRemovedFromSceneHandler(target    : ISceneNode,
                                                                  scene     : Scene) : void
        {
            super.targetRemovedFromSceneHandler(target, scene);

            if (getNumTargetsInScene(scene))
                _scene = null;
        }

		override protected function sceneEnterFrameHandler(scene		: Scene,
														   viewport		: Viewport,
														   destination	: BitmapData,
														   time			: Number) : void
		{
			_viewport = viewport;
			_deltaTime = _lastTime - time;
			_lastTime = time;

            beforeUpdate();

			var numTargets : uint = this.numTargets;

			for (var i : uint = 0; i < numTargets; ++i)
            {
                var target : ISceneNode = getTarget(i);

                if (target.scene)
                {
                    if (!_started[target])
                    {
                        _started[target] = true;
                        start(target);
                    }

    				update(target);
                }
                else if (_started[target])
                {
                    _started[target] = false;
                    stop(target);
                }
            }

            afterUpdate();
		}

        /**
         * The 'start' method is called on a script target at the first frame occuring after it
         * has been added to the scene.
         *
         * @param target
         *
         */
		protected function start(target : ISceneNode) : void
		{
			// nothing
		}

        /**
         * The 'beforeUpdate' method is called before each target is updated via the 'update'
         * method.
         *
         */
        protected function beforeUpdate() : void
        {
            // nothing
        }

		protected function update(target : ISceneNode) : void
		{
			// nothing
		}

        /**
         * The 'afterUpdate' method is called after each target has been updated via the 'update'
         * method.
         *
         */
        protected function afterUpdate() : void
        {
            // nothing
        }

        /**
         * The 'start' method is called on a script target at the first frame occuring after it
         * has been removed from the scene.
         *
         * @param target
         *
         */
		protected function stop(target : ISceneNode) : void
		{
			// nothing
		}
	}
}

fix missing init of ScriptController._started


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mangui.hls.demux {
    import flash.utils.ByteArray;

    CONFIG::LOGGING {
    import org.mangui.hls.utils.Log;
    }

    public class ID3 {
        public var len : int;
        public var hasTimestamp : Boolean = false;
        public var timestamp : Number;
        public var tags : Vector.<ID3Tag> = new Vector.<ID3Tag>();

        /* create ID3 object by parsing ByteArray, looking for ID3 tag length, and timestamp */
        public function ID3(data : ByteArray) {
            var tagSize : uint = 0;
            try {
                var pos : uint = data.position;
                var header : String;
                do {
                    header = data.readUTFBytes(3);
                    if (header == 'ID3') {
                        // skip 24 bits
                        data.position += 3;
                        // retrieve tag(s) length
                        var byte1 : uint = data.readUnsignedByte() & 0x7f;
                        var byte2 : uint = data.readUnsignedByte() & 0x7f;
                        var byte3 : uint = data.readUnsignedByte() & 0x7f;
                        var byte4 : uint = data.readUnsignedByte() & 0x7f;
                        tagSize = (byte1 << 21) + (byte2 << 14) + (byte3 << 7) + byte4;
                        var endPos : uint = data.position + tagSize;
                        CONFIG::LOGGING {
                        Log.debug2("ID3 tag found, size/end pos:" + tagSize + "/" + endPos);
                        }
                        // read ID3 tags
                        _parseID3Frames(data, endPos);
                        data.position = endPos;
                    } else if (header == '3DI') {
                        // http://id3.org/id3v2.4.0-structure chapter 3.4.   ID3v2 footer
                        data.position += 7;
                        CONFIG::LOGGING {
                            Log.debug2("3DI footer found, end pos:" + data.position);
                        }
                    } else {
                        data.position -= 3;
                        len = data.position - pos;
                        CONFIG::LOGGING {
                            if (len) {
                                Log.debug2("ID3 len:" + len);
                                if (!hasTimestamp) {
                                    Log.warn("ID3 tag found, but no timestamp");
                                }
                            }
                        }
                        return;
                    }
                } while (true);
            } catch(e : Error) {
            }
            len = 0;
            return;
        };

        /*  Each Elementary Audio Stream segment MUST signal the timestamp of
        its first sample with an ID3 PRIV tag [ID3] at the beginning of
        the segment.  The ID3 PRIV owner identifier MUST be
        "com.apple.streaming.transportStreamTimestamp".  The ID3 payload
        MUST be a 33-bit MPEG-2 Program Elementary Stream timestamp
        expressed as a big-endian eight-octet number, with the upper 31
        bits set to zero.
         */
        private function _parseID3Frames(data : ByteArray, endPos : uint) : void {
            while(data.position + 8 <= endPos) {
                var tag_id : String = data.readUTFBytes(4);
                var tag_len : int = data.readUnsignedInt();
                var tag_flags : int = data.readUnsignedShort();

                CONFIG::LOGGING {
                    Log.debug("ID3 tag id:" + tag_id);
                }
                switch(tag_id) {
                    case "PRIV":
                        // owner should be "com.apple.streaming.transportStreamTimestamp"
                        if (data.readUTFBytes(44) == 'com.apple.streaming.transportStreamTimestamp') {
                            // smelling even better ! we found the right descriptor
                            // skip null character (string end) + 3 first bytes
                            data.position += 4;
                            // timestamp is 33 bit expressed as a big-endian eight-octet number, with the upper 31 bits set to zero.
                            var pts_33_bit : int = data.readUnsignedByte() & 0x1;
                            hasTimestamp = true;
                            timestamp = (data.readUnsignedInt() / 90);
                            if (pts_33_bit) {
                                timestamp   += 47721858.84; // 2^32 / 90
                            }
                            CONFIG::LOGGING {
                                Log.debug("ID3 timestamp found:" + timestamp);
                            }
                        }
                        break;
                    default:
                        var tag_data : *;
                        var firstChar : String = tag_id.charAt(0);
                        if(firstChar == 'T' || firstChar == 'W') {
                            tag_data = new Array();
                            var cur_tag_pos : int;
                            var end_tag_pos : int = data.position + tag_len;
                            // skip character encoding byte
                            data.position++;
                            while(data.position < end_tag_pos - 1) {
                                cur_tag_pos = data.position;
                                var text : String = data.readUTFBytes(end_tag_pos-cur_tag_pos);
                                CONFIG::LOGGING {
                                    Log.debug("text:" + text);
                                }
                                data.position = cur_tag_pos + text.length + 1;
                                tag_data.push(text);
                            }
                            data.position = end_tag_pos;
                        } else {
                            tag_data = new ByteArray();
                            data.readBytes(tag_data, 0, tag_len);
                        }
                        tags.push(new ID3Tag(tag_id,tag_flags,tag_data));
                        break;
                }
            }
        }
    }
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mangui.hls.demux {
    import flash.utils.ByteArray;

    CONFIG::LOGGING {
    import org.mangui.hls.utils.Log;
    }

    public class ID3 {
        public var len : int;
        public var hasTimestamp : Boolean = false;
        public var timestamp : Number;
        public var tags : Vector.<ID3Tag> = new Vector.<ID3Tag>();

        /* create ID3 object by parsing ByteArray, looking for ID3 tag length, and timestamp */
        public function ID3(data : ByteArray) {
            var tagSize : uint = 0;
            try {
                var pos : uint = data.position;
                var header : String;
                do {
                    header = data.readUTFBytes(3);
                    if (header == 'ID3') {
                        // skip 24 bits
                        data.position += 3;
                        // retrieve tag(s) length
                        var byte1 : uint = data.readUnsignedByte() & 0x7f;
                        var byte2 : uint = data.readUnsignedByte() & 0x7f;
                        var byte3 : uint = data.readUnsignedByte() & 0x7f;
                        var byte4 : uint = data.readUnsignedByte() & 0x7f;
                        tagSize = (byte1 << 21) + (byte2 << 14) + (byte3 << 7) + byte4;
                        var endPos : uint = data.position + tagSize;
                        CONFIG::LOGGING {
                        Log.debug2("ID3 tag found, size/end pos:" + tagSize + "/" + endPos);
                        }
                        // read ID3 tags
                        _parseID3Frames(data, endPos);
                        data.position = endPos;
                    } else if (header == '3DI') {
                        // http://id3.org/id3v2.4.0-structure chapter 3.4.   ID3v2 footer
                        data.position += 7;
                        CONFIG::LOGGING {
                            Log.debug2("3DI footer found, end pos:" + data.position);
                        }
                    } else {
                        data.position -= 3;
                        len = data.position - pos;
                        CONFIG::LOGGING {
                            if (len) {
                                Log.debug2("ID3 len:" + len);
                                if (!hasTimestamp) {
                                    Log.warn("ID3 tag found, but no timestamp");
                                }
                            }
                        }
                        return;
                    }
                } while (true);
            } catch(e : Error) {
            }
            len = 0;
            return;
        };

        /*  Each Elementary Audio Stream segment MUST signal the timestamp of
        its first sample with an ID3 PRIV tag [ID3] at the beginning of
        the segment.  The ID3 PRIV owner identifier MUST be
        "com.apple.streaming.transportStreamTimestamp".  The ID3 payload
        MUST be a 33-bit MPEG-2 Program Elementary Stream timestamp
        expressed as a big-endian eight-octet number, with the upper 31
        bits set to zero.
         */
        private function _parseID3Frames(data : ByteArray, endPos : uint) : void {
            while(data.position + 8 <= endPos) {
                var tag_id : String = data.readUTFBytes(4);
                var tag_len : int = data.readUnsignedInt();
                var tag_flags : int = data.readUnsignedShort();

                CONFIG::LOGGING {
                    Log.debug("ID3 tag id:" + tag_id);
                }
                switch(tag_id) {
                    case "PRIV":
                        // owner should be "com.apple.streaming.transportStreamTimestamp"
                        if (data.readUTFBytes(44) == 'com.apple.streaming.transportStreamTimestamp') {
                            // smelling even better ! we found the right descriptor
                            // skip null character (string end) + 3 first bytes
                            data.position += 4;
                            // timestamp is 33 bit expressed as a big-endian eight-octet number, with the upper 31 bits set to zero.
                            var pts_33_bit : int = data.readUnsignedByte() & 0x1;
                            hasTimestamp = true;
                            timestamp = (data.readUnsignedInt() / 90);
                            if (pts_33_bit) {
                                timestamp   += 47721858.84; // 2^32 / 90
                            }
                            timestamp = Math.round(timestamp);
                            CONFIG::LOGGING {
                                Log.debug("ID3 timestamp found:" + timestamp);
                            }
                        }
                        break;
                    default:
                        var tag_data : *;
                        var firstChar : String = tag_id.charAt(0);
                        if(firstChar == 'T' || firstChar == 'W') {
                            tag_data = new Array();
                            var cur_tag_pos : int;
                            var end_tag_pos : int = data.position + tag_len;
                            // skip character encoding byte
                            data.position++;
                            while(data.position < end_tag_pos - 1) {
                                cur_tag_pos = data.position;
                                var text : String = data.readUTFBytes(end_tag_pos-cur_tag_pos);
                                CONFIG::LOGGING {
                                    Log.debug("text:" + text);
                                }
                                data.position = cur_tag_pos + text.length + 1;
                                tag_data.push(text);
                            }
                            data.position = end_tag_pos;
                        } else {
                            tag_data = new ByteArray();
                            data.readBytes(tag_data, 0, tag_len);
                        }
                        tags.push(new ID3Tag(tag_id,tag_flags,tag_data));
                        break;
                }
            }
        }
    }
}

fix multiple FRAGMENT_PLAYING event sent with audio only playlists round timestamp so that METADATA tag PTS is similar to first AAC tag PTS


package aerys.minko.scene.controller.mesh
{
	import aerys.minko.scene.controller.AbstractController;
	import aerys.minko.scene.node.Mesh;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.binding.DataProvider;
	import aerys.minko.type.math.Matrix4x4;

	public final class MeshController extends AbstractController
	{
		private var _data 			: DataProvider;
		private var _worldToLocal 	: Matrix4x4;

		public function MeshController()
		{
			super(Mesh);

			_worldToLocal = new Matrix4x4();
		}

		private function targetAddedHandler(ctrl	: MeshController,
											target	: Mesh) : void
		{
			target.addedToScene.add(targetAddedToSceneHandler);
		}

		private function targetAddedToSceneHandler(target	: Mesh,
												   scene	: Scene) : void
		{
			_data = new DataProvider();
			_data.setProperty('localToWorld', target.getLocalToWorldTransform());
			_data.setProperty('worldToLocal', target.getWorldToLocalTransform(_worldToLocal));

			target.bindings.addProvider(_data);
			target.localToWorldTransformChanged.add(localToWorldChangedHandler);
		}

		private function localToWorldChangedHandler(mesh : Mesh, localToWorld : Matrix4x4) : void
		{
			_data.setProperty('localToWorld', localToWorld);
			_data.setProperty('worldToLocal', _worldToLocal.copyFrom(localToWorld).invert());
		}
	}
}

package aerys.minko.scene.controller.mesh
{
	import aerys.minko.scene.controller.AbstractController;
	import aerys.minko.scene.node.Mesh;
	import aerys.minko.scene.node.Scene;
	import aerys.minko.type.binding.DataProvider;
	import aerys.minko.type.math.Matrix4x4;

	public final class MeshController extends AbstractController
	{
		private var _data 			: DataProvider;
		private var _worldToLocal 	: Matrix4x4;

		public function MeshController()
		{
			super(Mesh);

			initialize();
		}

		private function initialize() : void
		{
			_worldToLocal = new Matrix4x4();

			targetAdded.add(targetAddedHandler);
		}

		private function targetAddedHandler(ctrl	: MeshController,
											target	: Mesh) : void
		{
			target.addedToScene.add(targetAddedToSceneHandler);
			target.removedFromScene.add(targetRemovedFromSceneHandler);
		}

		private function targetAddedToSceneHandler(target	: Mesh,
												   scene	: Scene) : void
		{
			_data = new DataProvider();
			_data.setProperty('localToWorld', target.getLocalToWorldTransform());
			_data.setProperty('worldToLocal', target.getWorldToLocalTransform(_worldToLocal));
			target.bindings.addProvider(_data);

			target.localToWorldTransformChanged.add(localToWorldChangedHandler);
		}

		private function targetRemovedFromSceneHandler(target	: Mesh,
													   scene	: Scene) : void
		{
			target.localToWorldTransformChanged.remove(localToWorldChangedHandler);

			target.bindings.removeProvider(_data);
			_data = null;
		}

		private function localToWorldChangedHandler(mesh : Mesh, localToWorld : Matrix4x4) : void
		{
			_data.setProperty('localToWorld', localToWorld);
			_data.setProperty('worldToLocal', _worldToLocal.copyFrom(localToWorld).invert());
		}
	}
}

fix missing init. of MeshController

fix missing init. of MeshController


//
// $Id#

package com.threerings.flash.video {

import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.Shape;
import flash.display.Sprite;

import flash.events.Event;
import flash.events.MouseEvent;

import flash.geom.Point;
import flash.geom.Rectangle;

import com.threerings.util.Log;
import com.threerings.util.ValueEvent;

/**
 *
 * @eventType com.threerings.flash.video.VideoPlayerCodes.SIZE
 */
[Event(name="size", type="com.threerings.util.ValueEvent")]

/**
 * A simple video display.
 */
public class SimpleVideoDisplay extends Sprite
{
    public static const NATIVE_WIDTH :int = 320;

    public static const NATIVE_HEIGHT :int = 240;

    /**
     * Create a video displayer.
     */
    public function SimpleVideoDisplay (player :VideoPlayer)
    {
        _player = player;
        _player.addEventListener(VideoPlayerCodes.STATE, handlePlayerState);
        _player.addEventListener(VideoPlayerCodes.SIZE, handlePlayerSize);
        _player.addEventListener(VideoPlayerCodes.DURATION, handlePlayerDuration);
        _player.addEventListener(VideoPlayerCodes.POSITION, handlePlayerPosition);
        _player.addEventListener(VideoPlayerCodes.ERROR, handlePlayerError);

        addChild(_player.getDisplay());

        _mask = new Shape();
        this.mask = _mask;
        addChild(_mask);

        addEventListener(MouseEvent.ROLL_OVER, handleRollOver);
        addEventListener(MouseEvent.ROLL_OUT, handleRollOut);

        _hud = new Sprite();
        _hud.addEventListener(MouseEvent.CLICK, handleClick);
        redrawHUD();

        // create the mask...
        var g :Graphics = _mask.graphics;
        g.clear();
        g.beginFill(0xFFFFFF);
        g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT);
        g.endFill();

        g = this.graphics;
        g.beginFill(0xFF000000);
        g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT);
        g.endFill();

        // and update the HUD location (even if not currently showing)
        _hud.x = NATIVE_WIDTH/2;
        _hud.y = NATIVE_HEIGHT/2;

        _track = new Sprite();
        _track.x = PAD - _hud.x;
        _track.y = NATIVE_HEIGHT - PAD - TRACK_HEIGHT - _hud.y;
        g = _track.graphics;
        g.beginFill(0x000000, .7);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        g.endFill();
        g.lineStyle(2, 0xFFFFFF);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        // _track is not added to _hud until we know the duration

        const trackMask :Shape = new Shape();
        g = trackMask.graphics;
        g.beginFill(0xFFFFFF);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        _track.addChild(trackMask);
        _track.mask = trackMask;

        _knob = new Sprite();
        _knob.y = TRACK_HEIGHT / 2;
        g = _knob.graphics;
        g.lineStyle(1, 0xFFFFFF);
        g.beginFill(0x000099);
        g.drawCircle(0, 0, TRACK_HEIGHT/2 - 1);
        // _knob is not added to _track until we know the position

        _track.addEventListener(MouseEvent.CLICK, handleTrackClick);
        _knob.addEventListener(MouseEvent.MOUSE_DOWN, handleKnobDown);
    }

    /**
     * Stop playing our video.
     */
    public function unload () :void
    {
        _player.unload();
    }

    protected function handleRollOver (event :MouseEvent) :void
    {
        showHUD(true);
    }

    protected function handleRollOut (event :MouseEvent) :void
    {
        showHUD(false);
    }

    protected function handleClick (event :MouseEvent) :void
    {
        // the buck stops here!
        event.stopImmediatePropagation();

        switch (_player.getState()) {
        case VideoPlayerCodes.STATE_READY:
        case VideoPlayerCodes.STATE_PAUSED:
            _player.play();
            break;

        case VideoPlayerCodes.STATE_PLAYING:
            _player.pause();
            break;

        default:
            log.info("Click while player in unhandled state: " + _player.getState());
            break;
        }
    }

    protected function handleTrackClick (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        adjustSeek(event.localX);
    }

    protected function handleKnobDown (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        _dragging = true;
        _knob.startDrag(false, new Rectangle(0, TRACK_HEIGHT/2, TRACK_WIDTH, 0));
        addEventListener(Event.ENTER_FRAME, handleKnobSeekCheck);
        addEventListener(MouseEvent.MOUSE_UP, handleKnobUp);
    }

    protected function handleKnobUp (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        _dragging = false;
        _knob.stopDrag();
        removeEventListener(Event.ENTER_FRAME, handleKnobSeekCheck);
        removeEventListener(MouseEvent.MOUSE_UP, handleKnobUp);
        handleKnobSeekCheck(null);
    }

    protected function handleKnobSeekCheck (event :Event) :void
    {
        adjustSeek(_knob.x);
    }

    protected function handlePlayerState (event :ValueEvent) :void
    {
        redrawHUD();
    }

    protected function handlePlayerSize (event :ValueEvent) :void
    {
        const size :Point = Point(event.value);

        const disp :DisplayObject = _player.getDisplay();
        // TODO: siggggghhhhh
//        disp.scaleX = NATIVE_WIDTH / size.x;
//        disp.scaleY = NATIVE_HEIGHT / size.y;
        disp.width = NATIVE_WIDTH;
        disp.height = NATIVE_HEIGHT;

        // and, we redispatch
        dispatchEvent(event);
    }

    protected function handlePlayerDuration (event :ValueEvent) :void
    {
        _hud.addChild(_track);
    }

    protected function handlePlayerPosition (event :ValueEvent) :void
    {
        if (_dragging) {
            return;
        }
        _lastKnobX = int.MIN_VALUE;
        const pos :Number = Number(event.value);
        _knob.x = (pos / _player.getDuration()) * TRACK_WIDTH;
        if (_knob.parent == null) {
            _track.addChild(_knob);
        }
    }

    protected function handlePlayerError (event :ValueEvent) :void
    {
        // TODO.. maybe just redispatch
        log.warning("player error: " + event.value);
    }

    protected function adjustSeek (trackX :Number) :void
    {
        // see if we can seek to a position
        const dur :Number = _player.getDuration();
        if (isNaN(dur)) {
            log.debug("Duration is NaN, not seeking.");
            return;
        }
        if (trackX == _lastKnobX) {
            return;
        }
        _lastKnobX = trackX;
        var perc :Number = trackX / TRACK_WIDTH;
        perc = Math.max(0, Math.min(1, perc));
        log.debug("Seek", "x", trackX, "perc", perc, "pos", (perc * dur));
        _player.seek(perc * dur);
    }

    protected function showHUD (show :Boolean) :void
    {
        if (show == (_hud.parent == null)) {
            if (show) {
                addChild(_hud);
            } else {
                removeChild(_hud);
            }
        }
    }

    protected function redrawHUD () :void
    {
        const state :int = _player.getState();

        const g :Graphics = _hud.graphics;
        g.clear();

        if ((state != VideoPlayerCodes.STATE_READY) &&
                (state != VideoPlayerCodes.STATE_PLAYING) &&
                (state != VideoPlayerCodes.STATE_PAUSED)) {
            // draw something loading-like
            // TODO animated swf
            g.beginFill(0x000033);
            g.drawCircle(5, 5, 10);
            g.drawCircle(-5, 5, 10);
            g.drawCircle(-5, -5, 10);
            g.drawCircle(5, -5, 10);
            g.endFill();
            return;
        }

        // draw a nice circle
        g.beginFill(0x000000, .7);
        g.drawCircle(0, 0, 20);
        g.endFill();
        g.lineStyle(2, 0xFFFFFF);
        g.drawCircle(0, 0, 20);

        if (state == VideoPlayerCodes.STATE_PLAYING) {
            g.lineStyle(2, 0x00FF00);
            g.moveTo(-4, -10);
            g.lineTo(-4, 10);
            g.moveTo(4, -10);
            g.lineTo(4, 10);

        } else { // READY or PAUSED
            g.lineStyle(0, 0, 0);
            g.beginFill(0x00FF00);
            g.moveTo(-4, -10);
            g.lineTo(4, 0);
            g.lineTo(-4, 10);
            g.lineTo(-4, -10);
            g.endFill();
        }
    }

    protected const log :Log = Log.getLog(this);

    protected var _player :VideoPlayer;

    protected var _hud :Sprite;

    protected var _track :Sprite;

    /** The seek selector knob, positioned on the hud. */
    protected var _knob :Sprite;

    protected var _lastKnobX :int = int.MIN_VALUE;

    protected var _dragging :Boolean;

    /** Our mask, also defines our boundaries for clicking. */
    protected var _mask :Shape;

    protected static const PAD :int = 10;
    protected static const TRACK_HEIGHT :int = 20;
    protected static const TRACK_WIDTH :int = NATIVE_WIDTH - (PAD * 2);
}
}

//
// $Id#

package com.threerings.flash.video {

import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.Shape;
import flash.display.Sprite;

import flash.events.Event;
import flash.events.MouseEvent;

import flash.geom.Point;
import flash.geom.Rectangle;

import com.threerings.util.Log;
import com.threerings.util.ValueEvent;

/**
 *
 * @eventType com.threerings.flash.video.VideoPlayerCodes.SIZE
 */
[Event(name="size", type="com.threerings.util.ValueEvent")]

/**
 * A simple video display.
 */
public class SimpleVideoDisplay extends Sprite
{
    public static const NATIVE_WIDTH :int = 320;

    public static const NATIVE_HEIGHT :int = 240;

    /**
     * Create a video displayer.
     */
    public function SimpleVideoDisplay (player :VideoPlayer)
    {
        _player = player;
        _player.addEventListener(VideoPlayerCodes.STATE, handlePlayerState);
        _player.addEventListener(VideoPlayerCodes.SIZE, handlePlayerSize);
        _player.addEventListener(VideoPlayerCodes.DURATION, handlePlayerDuration);
        _player.addEventListener(VideoPlayerCodes.POSITION, handlePlayerPosition);
        _player.addEventListener(VideoPlayerCodes.ERROR, handlePlayerError);

        addChild(_player.getDisplay());

        _mask = new Shape();
        this.mask = _mask;
        addChild(_mask);

        addEventListener(MouseEvent.ROLL_OVER, handleRollOver);
        addEventListener(MouseEvent.ROLL_OUT, handleRollOut);

        _hud = new Sprite();
        _hud.addEventListener(MouseEvent.CLICK, handleClick);
        redrawHUD();

        // create the mask...
        var g :Graphics = _mask.graphics;
        g.clear();
        g.beginFill(0xFFFFFF);
        g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT);
        g.endFill();

        g = this.graphics;
        g.beginFill(0xFF000000);
        g.drawRect(0, 0, NATIVE_WIDTH, NATIVE_HEIGHT);
        g.endFill();

        // and update the HUD location (even if not currently showing)
        _hud.x = NATIVE_WIDTH/2;
        _hud.y = NATIVE_HEIGHT/2;

        _track = new Sprite();
        _track.x = PAD - _hud.x;
        _track.y = NATIVE_HEIGHT - PAD - TRACK_HEIGHT - _hud.y;
        g = _track.graphics;
        g.beginFill(0x000000, .7);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        g.endFill();
        g.lineStyle(2, 0xFFFFFF);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        // _track is not added to _hud until we know the duration

        const trackMask :Shape = new Shape();
        g = trackMask.graphics;
        g.beginFill(0xFFFFFF);
        g.drawRect(0, 0, TRACK_WIDTH, TRACK_HEIGHT);
        _track.addChild(trackMask);
        _track.mask = trackMask;

        _knob = new Sprite();
        _knob.y = TRACK_HEIGHT / 2;
        g = _knob.graphics;
        g.lineStyle(1, 0xFFFFFF);
        g.beginFill(0x000099);
        g.drawCircle(0, 0, TRACK_HEIGHT/2 - 1);
        // _knob is not added to _track until we know the position

        _track.addEventListener(MouseEvent.CLICK, handleTrackClick);
        _knob.addEventListener(MouseEvent.MOUSE_DOWN, handleKnobDown);
    }

    /**
     * Stop playing our video.
     */
    public function unload () :void
    {
        _player.unload();
    }

    protected function handleRollOver (event :MouseEvent) :void
    {
        showHUD(true);
    }

    protected function handleRollOut (event :MouseEvent) :void
    {
        showHUD(false);
    }

    protected function handleClick (event :MouseEvent) :void
    {
        // the buck stops here!
        event.stopImmediatePropagation();

        switch (_player.getState()) {
        case VideoPlayerCodes.STATE_READY:
        case VideoPlayerCodes.STATE_PAUSED:
            _player.play();
            break;

        case VideoPlayerCodes.STATE_PLAYING:
            _player.pause();
            break;

        default:
            log.info("Click while player in unhandled state: " + _player.getState());
            break;
        }
    }

    protected function handleTrackClick (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        adjustSeek(event.localX);
    }

    protected function handleKnobDown (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        _dragging = true;
        _knob.startDrag(false, new Rectangle(0, TRACK_HEIGHT/2, TRACK_WIDTH, 0));
        addEventListener(Event.ENTER_FRAME, handleKnobSeekCheck);
        addEventListener(MouseEvent.MOUSE_UP, handleKnobUp);
    }

    protected function handleKnobUp (event :MouseEvent) :void
    {
        event.stopImmediatePropagation();

        _dragging = false;
        _knob.stopDrag();
        removeEventListener(Event.ENTER_FRAME, handleKnobSeekCheck);
        removeEventListener(MouseEvent.MOUSE_UP, handleKnobUp);
        handleKnobSeekCheck(null);
    }

    protected function handleKnobSeekCheck (event :Event) :void
    {
        adjustSeek(_knob.x);
    }

    protected function handlePlayerState (event :ValueEvent) :void
    {
        redrawHUD();
    }

    protected function handlePlayerSize (event :ValueEvent) :void
    {
        const size :Point = Point(event.value);

        const disp :DisplayObject = _player.getDisplay();
        // TODO: siggggghhhhh
//        disp.scaleX = NATIVE_WIDTH / size.x;
//        disp.scaleY = NATIVE_HEIGHT / size.y;
        disp.width = NATIVE_WIDTH;
        disp.height = NATIVE_HEIGHT;

        // and, we redispatch
        dispatchEvent(event);
    }

    protected function handlePlayerDuration (event :ValueEvent) :void
    {
// TODO: temporarily disabled
//        _hud.addChild(_track);
    }

    protected function handlePlayerPosition (event :ValueEvent) :void
    {
        if (_dragging) {
            return;
        }
        _lastKnobX = int.MIN_VALUE;
        const pos :Number = Number(event.value);
        _knob.x = (pos / _player.getDuration()) * TRACK_WIDTH;
        if (_knob.parent == null) {
            _track.addChild(_knob);
        }
    }

    protected function handlePlayerError (event :ValueEvent) :void
    {
        // TODO.. maybe just redispatch
        log.warning("player error: " + event.value);
    }

    protected function adjustSeek (trackX :Number) :void
    {
        // see if we can seek to a position
        const dur :Number = _player.getDuration();
        if (isNaN(dur)) {
            log.debug("Duration is NaN, not seeking.");
            return;
        }
        if (trackX == _lastKnobX) {
            return;
        }
        _lastKnobX = trackX;
        var perc :Number = trackX / TRACK_WIDTH;
        perc = Math.max(0, Math.min(1, perc));
        log.debug("Seek", "x", trackX, "perc", perc, "pos", (perc * dur));
        _player.seek(perc * dur);
    }

    protected function showHUD (show :Boolean) :void
    {
        if (show == (_hud.parent == null)) {
            if (show) {
                addChild(_hud);
            } else {
                removeChild(_hud);
            }
        }
    }

    protected function redrawHUD () :void
    {
        const state :int = _player.getState();

        const g :Graphics = _hud.graphics;
        g.clear();

        if ((state != VideoPlayerCodes.STATE_READY) &&
                (state != VideoPlayerCodes.STATE_PLAYING) &&
                (state != VideoPlayerCodes.STATE_PAUSED)) {
            // draw something loading-like
            // TODO animated swf
            g.beginFill(0x000033);
            g.drawCircle(5, 5, 10);
            g.drawCircle(-5, 5, 10);
            g.drawCircle(-5, -5, 10);
            g.drawCircle(5, -5, 10);
            g.endFill();
            return;
        }

        // draw a nice circle
        g.beginFill(0x000000, .7);
        g.drawCircle(0, 0, 20);
        g.endFill();
        g.lineStyle(2, 0xFFFFFF);
        g.drawCircle(0, 0, 20);

        if (state == VideoPlayerCodes.STATE_PLAYING) {
            g.lineStyle(2, 0x00FF00);
            g.moveTo(-4, -10);
            g.lineTo(-4, 10);
            g.moveTo(4, -10);
            g.lineTo(4, 10);

        } else { // READY or PAUSED
            g.lineStyle(0, 0, 0);
            g.beginFill(0x00FF00);
            g.moveTo(-4, -10);
            g.lineTo(4, 0);
            g.lineTo(-4, 10);
            g.lineTo(-4, -10);
            g.endFill();
        }
    }

    protected const log :Log = Log.getLog(this);

    protected var _player :VideoPlayer;

    protected var _hud :Sprite;

    protected var _track :Sprite;

    /** The seek selector knob, positioned on the hud. */
    protected var _knob :Sprite;

    protected var _lastKnobX :int = int.MIN_VALUE;

    protected var _dragging :Boolean;

    /** Our mask, also defines our boundaries for clicking. */
    protected var _mask :Shape;

    protected static const PAD :int = 10;
    protected static const TRACK_HEIGHT :int = 20;
    protected static const TRACK_WIDTH :int = NATIVE_WIDTH - (PAD * 2);
}
}

Comment out the advanced controls, they're not ready for prime-time.

git-svn-id: b675b909355d5cf946977f44a8ec5a6ceb3782e4@687 ed5b42cb-e716-0410-a449-f6a68f950b19