package com.mangui.HLS.streaming {

    import com.mangui.HLS.*;
    import com.mangui.HLS.parsing.*;
    import com.mangui.HLS.utils.*;
    import flash.events.*;
    import flash.net.*;
    import flash.utils.*;

    /** Loader for hls manifests. **/
    public class ManifestLoader {

        /** Reference to the hls framework controller. **/
        private var _hls:HLS;
        /** Array with levels. **/
        private var _levels:Array = [];
        /** Object that fetches the manifest. **/
        private var _urlloader:URLLoader;
        /** Link to the M3U8 file. **/
        private var _url:String;
        /** Amount of playlists still need loading. **/
        private var _toLoad:Number;
        /** are all playlists filled ? **/
        private var _canStart:Boolean;
        /** initial reload timer **/
        private var _fragmentDuration:Number = 5000;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID:Number;
        /** Streaming type (live, ondemand). **/
        private var _type:String;
        /** last reload manifest time **/
        private var _reload_playlists_timer:uint;

        /** Setup the loader. **/
        public function ManifestLoader(hls:HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.STATE,_stateHandler);
            _levels = [];
            _urlloader = new URLLoader();
            _urlloader.addEventListener(Event.COMPLETE,_loaderHandler);
            _urlloader.addEventListener(IOErrorEvent.IO_ERROR,_errorHandler);
            _urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,_errorHandler);
        };

        /** Loading failed; return errors. **/
        private function _errorHandler(event:ErrorEvent):void {
            var txt:String = "Cannot load M3U8: "+event.text;
            if(event is SecurityErrorEvent) {
                txt = "Cannot load M3U8: crossdomain access denied";
            } else if (event is IOErrorEvent) {
                txt = "Cannot load M3U8: 404 not found";
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR,txt));
        };

        /** return true if first two levels contain at least 2 fragments **/
        private function _areFirstLevelsFilled():Boolean {
           for(var i:Number = 0; (i < _levels.length) && (i < 2); i++) {
            if(_levels[i].fragments.length < 2) {
               return false;
            }
         }
         return true;
        };

        /** Return the current manifest. **/
        public function getLevels():Array {
            return _levels;
        };

        /** Return the stream type. **/
        public function getType():String {
            return _type;
        };

        /** Load the manifest file. **/
        public function load(url:String):void {
            _url = url;
            _levels = [];
            _canStart = false;
            _reload_playlists_timer = getTimer();
            _urlloader.load(new URLRequest(_url));
        };

        /** Manifest loaded; check and parse it **/
        private function _loaderHandler(event:Event):void {
            _loadManifest(String(event.target.data));
        };

        /** load a playlist **/
        private function _loadPlaylist(string:String,url:String,index:Number):void {
            if(string != null && string.length != 0) {
               var frags:Array = Manifest.getFragments(string,url);
               // set fragment and update sequence number range
               _levels[index].setFragments(frags);
               _levels[index].targetduration = Manifest.getTargetDuration(string);
               _levels[index].start_seqnum = frags[0].seqnum;
               _levels[index].end_seqnum = frags[frags.length-1].seqnum;
               _levels[index].duration = frags[frags.length-1].start + frags[frags.length-1].duration;
               _fragmentDuration = _levels[index].duration/frags.length;
               _levels[index].averageduration = _fragmentDuration;
            }
            if(--_toLoad == 0) {
            // Check whether the stream is live or not finished yet
            if(Manifest.hasEndlist(string)) {
                _type = HLSTypes.VOD;
            } else {
                _type = HLSTypes.LIVE;
                var timeout:Number = Math.max(100,_reload_playlists_timer + _fragmentDuration - getTimer());
                Log.txt("Live Playlist parsing finished: reload in " + timeout + " ms");
                _timeoutID = setTimeout(_loadPlaylists,timeout);
            }
               if (!_canStart && (_canStart =_areFirstLevelsFilled())) {
                  Log.txt("first 2 levels are filled with at least 2 fragments, notify event");
                  _hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST,_levels));
              }
            }
        };

        /** load First Level Playlist **/
        private function _loadManifest(string:String):void {
            // Check for M3U8 playlist or manifest.
            if(string.indexOf(Manifest.HEADER) == 0) {
               //1 level playlist, create unique level and parse playlist
                if(string.indexOf(Manifest.FRAGMENT) > 0) {
                    var level:Level = new Level();
                    level.url = _url;
                    _levels.push(level);
                    _toLoad = 1;
                    Log.txt("1 Level Playlist, load it");
                    _loadPlaylist(string,_url,0);
                } else if(string.indexOf(Manifest.LEVEL) > 0) {
                  //adaptative playlist, extract levels from playlist, get them and parse them
                  _levels = Manifest.extractLevels(string,_url);
                  _loadPlaylists();
                }
            } else {
                var message:String = "Manifest is not a valid M3U8 file" + _url;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR,message));
            }
        };

        /** load/reload all M3U8 playlists **/
        private function _loadPlaylists():void {
            _reload_playlists_timer = getTimer();
            _toLoad = _levels.length;
            //Log.txt("HLS Playlist, with " + _toLoad + " levels");
            for(var i:Number = 0; i < _levels.length; i++) {
                new Manifest().loadPlaylist(_levels[i].url,_loadPlaylist,_errorHandler,i);
            }
        };

        /** When the framework idles out, reloading is cancelled. **/
        public function _stateHandler(event:HLSEvent):void {
            if(event.state == HLSStates.IDLE) {
                clearTimeout(_timeoutID);
            }
        };
    }
}

package com.mangui.HLS.streaming {

    import com.mangui.HLS.*;
    import com.mangui.HLS.parsing.*;
    import com.mangui.HLS.utils.*;
    import flash.events.*;
    import flash.net.*;
    import flash.utils.*;

    /** Loader for hls manifests. **/
    public class ManifestLoader {

        /** Reference to the hls framework controller. **/
        private var _hls:HLS;
        /** Array with levels. **/
        private var _levels:Array = [];
        /** Object that fetches the manifest. **/
        private var _urlloader:URLLoader;
        /** Link to the M3U8 file. **/
        private var _url:String;
        /** Amount of playlists still need loading. **/
        private var _toLoad:Number;
        /** are all playlists filled ? **/
        private var _canStart:Boolean;
        /** initial reload timer **/
        private var _fragmentDuration:Number = 5000;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID:Number;
        /** Streaming type (live, ondemand). **/
        private var _type:String;
        /** last reload manifest time **/
        private var _reload_playlists_timer:uint;

        /** Setup the loader. **/
        public function ManifestLoader(hls:HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.STATE,_stateHandler);
            _levels = [];
            _urlloader = new URLLoader();
            _urlloader.addEventListener(Event.COMPLETE,_loaderHandler);
            _urlloader.addEventListener(IOErrorEvent.IO_ERROR,_errorHandler);
            _urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,_errorHandler);
        };

        /** Loading failed; return errors. **/
        private function _errorHandler(event:ErrorEvent):void {
            var txt:String = "Cannot load M3U8: "+event.text;
            if(event is SecurityErrorEvent) {
                txt = "Cannot load M3U8: crossdomain access denied";
            } else if (event is IOErrorEvent) {
                txt = "Cannot load M3U8: 404 not found";
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR,txt));
        };

        /** return true if first two levels contain at least 2 fragments **/
        private function _areFirstLevelsFilled():Boolean {
           for(var i:Number = 0; (i < _levels.length) && (i < 2); i++) {
            if(_levels[i].fragments.length < 2) {
               return false;
            }
         }
         return true;
        };

        /** Return the current manifest. **/
        public function getLevels():Array {
            return _levels;
        };

        /** Return the stream type. **/
        public function getType():String {
            return _type;
        };

        /** Load the manifest file. **/
        public function load(url:String):void {
            _url = url;
            _levels = [];
            _canStart = false;
            _reload_playlists_timer = getTimer();
            _urlloader.load(new URLRequest(_url));
        };

        /** Manifest loaded; check and parse it **/
        private function _loaderHandler(event:Event):void {
            _loadManifest(String(event.target.data));
        };

        /** load a playlist **/
        private function _loadPlaylist(string:String,url:String,index:Number):void {
            if(string != null && string.length != 0) {
               var frags:Array = Manifest.getFragments(string,url);
               // set fragment and update sequence number range
               _levels[index].setFragments(frags);
               _levels[index].targetduration = Manifest.getTargetDuration(string);
               _levels[index].start_seqnum = frags[0].seqnum;
               _levels[index].end_seqnum = frags[frags.length-1].seqnum;
               _levels[index].duration = frags[frags.length-1].start + frags[frags.length-1].duration;
               _fragmentDuration = _levels[index].duration/frags.length;
               _levels[index].averageduration = _fragmentDuration;
            }
            if(--_toLoad == 0) {
            // Check whether the stream is live or not finished yet
            if(Manifest.hasEndlist(string)) {
                _type = HLSTypes.VOD;
            } else {
                _type = HLSTypes.LIVE;
                var timeout:Number = Math.max(100,_reload_playlists_timer + 1000*_fragmentDuration - getTimer());
                Log.txt("Live Playlist parsing finished: reload in " + timeout + " ms");
                _timeoutID = setTimeout(_loadPlaylists,timeout);
            }
               if (!_canStart && (_canStart =_areFirstLevelsFilled())) {
                  Log.txt("first 2 levels are filled with at least 2 fragments, notify event");
                  _hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST,_levels));
              }
            }
        };

        /** load First Level Playlist **/
        private function _loadManifest(string:String):void {
            // Check for M3U8 playlist or manifest.
            if(string.indexOf(Manifest.HEADER) == 0) {
               //1 level playlist, create unique level and parse playlist
                if(string.indexOf(Manifest.FRAGMENT) > 0) {
                    var level:Level = new Level();
                    level.url = _url;
                    _levels.push(level);
                    _toLoad = 1;
                    Log.txt("1 Level Playlist, load it");
                    _loadPlaylist(string,_url,0);
                } else if(string.indexOf(Manifest.LEVEL) > 0) {
                  //adaptative playlist, extract levels from playlist, get them and parse them
                  _levels = Manifest.extractLevels(string,_url);
                  _loadPlaylists();
                }
            } else {
                var message:String = "Manifest is not a valid M3U8 file" + _url;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR,message));
            }
        };

        /** load/reload all M3U8 playlists **/
        private function _loadPlaylists():void {
            _reload_playlists_timer = getTimer();
            _toLoad = _levels.length;
            //Log.txt("HLS Playlist, with " + _toLoad + " levels");
            for(var i:Number = 0; i < _levels.length; i++) {
                new Manifest().loadPlaylist(_levels[i].url,_loadPlaylist,_errorHandler,i);
            }
        };

        /** When the framework idles out, reloading is cancelled. **/
        public function _stateHandler(event:HLSEvent):void {
            if(event.state == HLSStates.IDLE) {
                clearTimeout(_timeoutID);
            }
        };
    }
}

fix reload timer too small

fix reload timer too small


//////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2012 Freshplanet (http://freshplanet.com | opensource@freshplanet.com)
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
//////////////////////////////////////////////////////////////////////////////////////

package com.freshplanet.ane.AirInAppPurchase {

	import flash.events.EventDispatcher;
	import flash.events.StatusEvent;
	import flash.external.ExtensionContext;
	import flash.system.Capabilities;

	public class InAppPurchase extends EventDispatcher {

		private var _context:ExtensionContext = null;
        private var _iosPendingPurchases:Vector.<Object> = new Vector.<Object>();

        private static const EXTENSION_ID:String = "com.freshplanet.AirInAppPurchase";

        private static var _instance:InAppPurchase = null;

		public function InAppPurchase(lock:SingletonLock) {

            if (!isSupported)
                return;

            _context = ExtensionContext.createExtensionContext(EXTENSION_ID, null);

            if (!_context)
                throw Error("Extension context is null. Please check if extension.xml is setup correctly.");

            _context.addEventListener(StatusEvent.STATUS, _onStatus);
		}

        public static function get instance():InAppPurchase {

            if (!_instance)
                _instance = new InAppPurchase(new SingletonLock());

            return _instance;
        }

        public static function get isSupported():Boolean {
            return _isIOS() || _isAndroid();
        }

        private static function _isIOS():Boolean {
            return Capabilities.manufacturer.indexOf("iOS") > -1;
        }

        private static function _isAndroid():Boolean {
            return Capabilities.manufacturer.indexOf("Android") > -1;
        }

        /**
         * INIT_SUCCESSFUL
         * INIT_ERROR
         * @param googlePlayKey
         * @param debug
         */
		public function init(googlePlayKey:String, debug:Boolean = false):void {

            if (!isSupported)
                return;

            trace("[InAppPurchase] init library");
            _context.call("initLib", googlePlayKey, debug);
		}

        /**
         * PURCHASE_SUCCESSFUL
         * PURCHASE_ERROR
         * @param productId
         */
		public function makePurchase(productId:String):void {

            if (!isSupported) {

                _dispatchEvent(InAppPurchaseEvent.PURCHASE_ERROR, "InAppPurchase not supported");
                return;
            }

            trace("[InAppPurchase] purchasing", productId);
            _context.call("makePurchase", productId);
		}

        /**
         * PURCHASE_SUCCESSFUL
         * PURCHASE_ERROR
         * @param productId
         */
        public function makeSubscription(productId:String):void {

            if (_isAndroid()) {

                trace("[InAppPurchase] check user can make a subscription");
                _context.call("makeSubscription", productId);
            }
            else {
                _dispatchEvent(InAppPurchaseEvent.PURCHASE_ERROR, "subscriptions not supported");
            }
        }

        /**
         * CONSUME_SUCCESSFUL
         * CONSUME_ERROR
         * @param productId
         * @param receipt
         */
		public function removePurchaseFromQueue(productId:String, receipt:String):void {

            if (!isSupported)
                return;

            trace("[InAppPurchase] removing product from queue", productId, receipt);
            _context.call("removePurchaseFromQueue", productId, receipt);

            if (Capabilities.manufacturer.indexOf("iOS") > -1) {

                var filterPurchase:Function = function(jsonPurchase:String, index:int, purchases:Vector.<Object>):Boolean {

                    try {

                        var purchase:Object = JSON.parse(jsonPurchase);
                        return JSON.stringify(purchase.receipt) != receipt;
                    }
                    catch (error:Error) {
                        trace("[InAppPurchase] Couldn't parse purchase: " + jsonPurchase);
                    }

                    return false;
                };

                _iosPendingPurchases = _iosPendingPurchases.filter(filterPurchase);

            }
		}

        /**
         * PRODUCT_INFO_RECEIVED
         * PRODUCT_INFO_ERROR
         * @param productsId
         * @param subscriptionIds
         */
		public function getProductsInfo(productsId:Array, subscriptionIds:Array):void {

            if (!isSupported) {

                _dispatchEvent(InAppPurchaseEvent.PRODUCT_INFO_ERROR, "InAppPurchase not supported");
                return;
            }

            trace("[InAppPurchase] get Products Info");
            if(!productsId) productsId = [];
            if(!subscriptionIds) subscriptionIds = [];
            _context.call("getProductsInfo", productsId, subscriptionIds);
		}

        /**
         * RESTORE_INFO_RECEIVED
         * RESTORE_INFO_ERROR
         */
		public function restoreTransactions():void {

			if (_isAndroid())
				_context.call("restoreTransaction");
			else if (_isIOS()) {

				var jsonPurchases:String = "[" + _iosPendingPurchases.join(",") + "]";
				var jsonData:String = "{ \"purchases\": " + jsonPurchases + "}";

                _dispatchEvent(InAppPurchaseEvent.RESTORE_INFO_RECEIVED, jsonData);
			}
		}

        /**
         *
         * @param type
         * @param eventData
         */
        private function _dispatchEvent(type:String, eventData:String):void {
            this.dispatchEvent(new InAppPurchaseEvent(type, eventData))
        }

        /**
         *
         * @param event
         */
		private function _onStatus(event:StatusEvent):void {

			trace(event);

            if (event.code == InAppPurchaseEvent.PURCHASE_SUCCESSFUL && _isIOS())
                _iosPendingPurchases.push(event.level);

            _dispatchEvent(event.code, event.level);
		}
	}
}

class SingletonLock {}

//////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2012 Freshplanet (http://freshplanet.com | opensource@freshplanet.com)
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
//////////////////////////////////////////////////////////////////////////////////////

package com.freshplanet.ane.AirInAppPurchase {

	import flash.events.EventDispatcher;
	import flash.events.StatusEvent;
	import flash.external.ExtensionContext;
	import flash.system.Capabilities;

	public class InAppPurchase extends EventDispatcher {

		private var _context:ExtensionContext = null;
        private var _iosPendingPurchases:Vector.<Object> = new Vector.<Object>();

        private static const EXTENSION_ID:String = "com.freshplanet.AirInAppPurchase";

        private static var _instance:InAppPurchase = null;

		public function InAppPurchase(lock:SingletonLock) {

            if (!isSupported)
                return;

            _context = ExtensionContext.createExtensionContext(EXTENSION_ID, null);

            if (!_context)
                throw Error("Extension context is null. Please check if extension.xml is setup correctly.");

            _context.addEventListener(StatusEvent.STATUS, _onStatus);
		}

        public static function get instance():InAppPurchase {

            if (!_instance)
                _instance = new InAppPurchase(new SingletonLock());

            return _instance;
        }

        public static function get isSupported():Boolean {
            return _isIOS() || _isAndroid();
        }

        private static function _isIOS():Boolean {
            return Capabilities.manufacturer.indexOf("iOS") > -1;
        }

        private static function _isAndroid():Boolean {
            return Capabilities.manufacturer.indexOf("Android") > -1;
        }

        /**
         * INIT_SUCCESSFUL
         * INIT_ERROR
         * @param googlePlayKey
         * @param debug
         */
		public function init(googlePlayKey:String, debug:Boolean = false):void {

            if (!isSupported)
                return;

            trace("[InAppPurchase] init library");
            _context.call("initLib", googlePlayKey, debug);
		}

        /**
         * PURCHASE_SUCCESSFUL
         * PURCHASE_ERROR
         * @param productId
         */
		public function makePurchase(productId:String):void {

            if (!isSupported) {

                _dispatchEvent(InAppPurchaseEvent.PURCHASE_ERROR, "InAppPurchase not supported");
                return;
            }

            trace("[InAppPurchase] purchasing", productId);
            _context.call("makePurchase", productId);
		}

        /**
         * PURCHASE_SUCCESSFUL
         * PURCHASE_ERROR
         * @param productId
         */
        public function makeSubscription(productId:String):void {

            _context.call("makeSubscription", productId);

            /*
            if (_isAndroid()) {

                trace("[InAppPurchase] check user can make a subscription");
                _context.call("makeSubscription", productId);
            }
            else {
                _dispatchEvent(InAppPurchaseEvent.PURCHASE_ERROR, "subscriptions not supported");
            }
            */
        }

        /**
         * CONSUME_SUCCESSFUL
         * CONSUME_ERROR
         * @param productId
         * @param receipt
         */
		public function removePurchaseFromQueue(productId:String, receipt:String):void {

            if (!isSupported)
                return;

            trace("[InAppPurchase] removing product from queue", productId, receipt);
            _context.call("removePurchaseFromQueue", productId, receipt);

            if (Capabilities.manufacturer.indexOf("iOS") > -1) {

                var filterPurchase:Function = function(jsonPurchase:String, index:int, purchases:Vector.<Object>):Boolean {

                    try {

                        var purchase:Object = JSON.parse(jsonPurchase);
                        return JSON.stringify(purchase.receipt) != receipt;
                    }
                    catch (error:Error) {
                        trace("[InAppPurchase] Couldn't parse purchase: " + jsonPurchase);
                    }

                    return false;
                };

                _iosPendingPurchases = _iosPendingPurchases.filter(filterPurchase);

            }
		}

        /**
         * PRODUCT_INFO_RECEIVED
         * PRODUCT_INFO_ERROR
         * @param productsId
         * @param subscriptionIds
         */
		public function getProductsInfo(productsId:Array, subscriptionIds:Array):void {

            if (!isSupported) {

                _dispatchEvent(InAppPurchaseEvent.PRODUCT_INFO_ERROR, "InAppPurchase not supported");
                return;
            }

            trace("[InAppPurchase] get Products Info");
            _context.call("getProductsInfo", productsId, subscriptionIds);
		}

        /**
         * RESTORE_INFO_RECEIVED
         * RESTORE_INFO_ERROR
         */
		public function restoreTransactions():void {

			if (_isAndroid())
				_context.call("restoreTransaction");
			else if (_isIOS()) {

				var jsonPurchases:String = "[" + _iosPendingPurchases.join(",") + "]";
				var jsonData:String = "{ \"purchases\": " + jsonPurchases + "}";

                _dispatchEvent(InAppPurchaseEvent.RESTORE_INFO_RECEIVED, jsonData);
			}
		}

        /**
         *
         * @param type
         * @param eventData
         */
        private function _dispatchEvent(type:String, eventData:String):void {
            this.dispatchEvent(new InAppPurchaseEvent(type, eventData))
        }

        /**
         *
         * @param event
         */
		private function _onStatus(event:StatusEvent):void {

			trace(event);

            if (event.code == InAppPurchaseEvent.PURCHASE_SUCCESSFUL && _isIOS())
                _iosPendingPurchases.push(event.level);

            _dispatchEvent(event.code, event.level);
		}
	}
}

class SingletonLock {}

allow subscriptions for ios

allow subscriptions for ios


package org.mangui.HLS.streaming {
    import org.mangui.HLS.*;
    import org.mangui.HLS.parsing.*;
    import org.mangui.HLS.utils.*;

    import flash.events.*;
    import flash.net.*;
    import flash.utils.*;

    /** Loader for hls manifests. **/
    public class ManifestLoader {
        /** Reference to the hls framework controller. **/
        private var _hls : HLS;
        /** levels vector. **/
        private var _levels : Vector.<Level>;
        /** Object that fetches the manifest. **/
        private var _urlloader : URLLoader;
        /** Link to the M3U8 file. **/
        private var _url : String;
        /** are all playlists filled ? **/
        private var _canStart : Boolean;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID : Number;
        /** Streaming type (live, ondemand). **/
        private var _type : String;
        /** last reload manifest time **/
        private var _reload_playlists_timer : uint;
        /** current level **/
        private var _current_level : Number;
        /** current level **/
        private var _load_in_progress : Boolean = false;
        /** flush live URL cache **/
        private var _flushLiveURLCache : Boolean = false;

        /** Setup the loader. **/
        public function ManifestLoader(hls : HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.QUALITY_SWITCH, _levelSwitchHandler);
            _levels = new Vector.<Level>();
            _urlloader = new URLLoader();
            _urlloader.addEventListener(Event.COMPLETE, _loaderHandler);
            _urlloader.addEventListener(IOErrorEvent.IO_ERROR, _errorHandler);
            _urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _errorHandler);
        };

        /** Loading failed; return errors. **/
        private function _errorHandler(event : ErrorEvent) : void {
            var txt : String;
            if (event is SecurityErrorEvent) {
                var error : SecurityErrorEvent = event as SecurityErrorEvent;
                txt = "Cannot load M3U8: crossdomain access denied:" + error.text;
            } else if (event is IOErrorEvent && _levels.length) {
                Log.warn("I/O Error while trying to load Playlist, retry in 2s");
                _timeoutID = setTimeout(_loadActiveLevelPlaylist, 2000);
            } else {
                txt = "Cannot load M3U8: " + event.text;
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, txt));
        };

        /** Return the current manifest. **/
        public function get levels() : Vector.<Level> {
            return _levels;
        };

        /** Return the stream type. **/
        public function get type() : String {
            return _type;
        };

        /** Load the manifest file. **/
        public function load(url : String) : void {
            _url = url;
            _levels = new Vector.<Level>();
            _current_level = 0;
            _canStart = false;
            _reload_playlists_timer = getTimer();
            _urlloader.load(new URLRequest(url));
        };

        /** Manifest loaded; check and parse it **/
        private function _loaderHandler(event : Event) : void {
            var loader : URLLoader = URLLoader(event.target);
            _parseManifest(String(loader.data));
        };

        /** parse a playlist **/
        private function _parseLevelPlaylist(string : String, url : String, index : Number) : void {
            if (string != null && string.length != 0) {
                Log.debug("level " + index + " playlist:\n" + string);
                var frags : Vector.<Fragment> = Manifest.getFragments(string, url);
                // set fragment and update sequence number range
                _levels[index].updateFragments(frags);
                _levels[index].targetduration = Manifest.getTargetDuration(string);
                _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYLIST_DURATION_UPDATED, _levels[index].duration));
            }

            // Check whether the stream is live or not finished yet
            if (Manifest.hasEndlist(string)) {
                _type = HLSTypes.VOD;
            } else {
                _type = HLSTypes.LIVE;
                var timeout : Number = Math.max(100, _reload_playlists_timer + 1000 * _levels[index].averageduration - getTimer());
                Log.debug("Level " + index + " Live Playlist parsing finished: reload in " + timeout.toFixed(0) + " ms");
                _timeoutID = setTimeout(_loadActiveLevelPlaylist, timeout);
            }
            if (!_canStart) {
                _canStart = (_levels[index].fragments.length > 0);
                if (_canStart) {
                    Log.debug("first level filled with at least 1 fragment, notify event");
                    _hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_LOADED, _levels));
                }
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_UPDATED, index));
            _load_in_progress = false;
        };

        /** Parse First Level Playlist **/
        private function _parseManifest(string : String) : void {
            // Check for M3U8 playlist or manifest.
            if (string.indexOf(Manifest.HEADER) == 0) {
                // 1 level playlist, create unique level and parse playlist
                if (string.indexOf(Manifest.FRAGMENT) > 0) {
                    var level : Level = new Level();
                    level.url = _url;
                    _levels.push(level);
                    Log.debug("1 Level Playlist, load it");
                    _parseLevelPlaylist(string, _url, 0);
                } else if (string.indexOf(Manifest.LEVEL) > 0) {
                    Log.debug("adaptive playlist:\n" + string);
                    // adaptative playlist, extract levels from playlist, get them and parse them
                    _levels = Manifest.extractLevels(string, _url);
                    _loadActiveLevelPlaylist();
                    if (string.indexOf(Manifest.ALTERNATE_AUDIO) > 0) {
                        Log.debug("alternate audio level found");
                        // parse alternate audio tracks
                        var altAudiolevels : Vector.<AltAudioTrack> = Manifest.extractAltAudioTracks(string, _url);
                        _hls.dispatchEvent(new HLSEvent(HLSEvent.ALT_AUDIO_TRACKS_LIST_CHANGE, altAudiolevels));
                    }
                }
            } else {
                var message : String = "Manifest is not a valid M3U8 file" + _url;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, message));
            }
        };

        /** load/reload active M3U8 playlist **/
        private function _loadActiveLevelPlaylist() : void {
            _load_in_progress = true;
            _reload_playlists_timer = getTimer();
            // load active M3U8 playlist only
            new Manifest().loadPlaylist(_levels[_current_level].url, _parseLevelPlaylist, _errorHandler, _current_level, _type, _flushLiveURLCache);
        };

        /** When level switch occurs, assess the need of (re)loading new level playlist **/
        public function _levelSwitchHandler(event : HLSEvent) : void {
            if (_current_level != event.level) {
                _current_level = event.level;
                Log.debug("switch to level " + _current_level);
                if (_type == HLSTypes.LIVE || _levels[_current_level].fragments.length == 0) {
                    Log.debug("(re)load Playlist");
                    clearTimeout(_timeoutID);
                    _timeoutID = setTimeout(_loadActiveLevelPlaylist, 0);
                }
            }
        };

        /** When the framework idles out, reloading is cancelled. **/
        public function _stateHandler(event : HLSEvent) : void {
            if (event.state == HLSStates.IDLE) {
                clearTimeout(_timeoutID);
            }
        };

        public function set flushLiveURLCache(val : Boolean) : void {
            _flushLiveURLCache = val;
        }

        public function get flushLiveURLCache() : Boolean {
            return _flushLiveURLCache;
        }
    }
}

package org.mangui.HLS.streaming {
    import org.mangui.HLS.*;
    import org.mangui.HLS.parsing.*;
    import org.mangui.HLS.utils.*;

    import flash.events.*;
    import flash.net.*;
    import flash.utils.*;

    /** Loader for hls manifests. **/
    public class ManifestLoader {
        /** Reference to the hls framework controller. **/
        private var _hls : HLS;
        /** levels vector. **/
        private var _levels : Vector.<Level>;
        /** Object that fetches the manifest. **/
        private var _urlloader : URLLoader;
        /** Link to the M3U8 file. **/
        private var _url : String;
        /** are all playlists filled ? **/
        private var _canStart : Boolean;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID : Number;
        /** Streaming type (live, ondemand). **/
        private var _type : String;
        /** last reload manifest time **/
        private var _reload_playlists_timer : uint;
        /** current level **/
        private var _current_level : Number;
        /** current level **/
        private var _load_in_progress : Boolean = false;
        /** flush live URL cache **/
        private var _flushLiveURLCache : Boolean = false;

        /** Setup the loader. **/
        public function ManifestLoader(hls : HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.QUALITY_SWITCH, _levelSwitchHandler);
            _levels = new Vector.<Level>();
            _urlloader = new URLLoader();
            _urlloader.addEventListener(Event.COMPLETE, _loaderHandler);
            _urlloader.addEventListener(IOErrorEvent.IO_ERROR, _errorHandler);
            _urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _errorHandler);
        };

        /** Loading failed; return errors. **/
        private function _errorHandler(event : ErrorEvent) : void {
            var txt : String;
            if (event is SecurityErrorEvent) {
                var error : SecurityErrorEvent = event as SecurityErrorEvent;
                txt = "Cannot load M3U8: crossdomain access denied:" + error.text;
            } else if (event is IOErrorEvent && _levels.length) {
                Log.warn("I/O Error while trying to load Playlist, retry in 2s");
                _timeoutID = setTimeout(_loadActiveLevelPlaylist, 2000);
            } else {
                txt = "Cannot load M3U8: " + event.text;
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, txt));
        };

        /** Return the current manifest. **/
        public function get levels() : Vector.<Level> {
            return _levels;
        };

        /** Return the stream type. **/
        public function get type() : String {
            return _type;
        };

        /** Load the manifest file. **/
        public function load(url : String) : void {
            _url = url;
            _levels = new Vector.<Level>();
            _current_level = 0;
            _canStart = false;
            _reload_playlists_timer = getTimer();
            _urlloader.load(new URLRequest(url));
        };

        /** Manifest loaded; check and parse it **/
        private function _loaderHandler(event : Event) : void {
            var loader : URLLoader = URLLoader(event.target);
            _parseManifest(String(loader.data));
        };

        /** parse a playlist **/
        private function _parseLevelPlaylist(string : String, url : String, index : Number) : void {
            if (string != null && string.length != 0) {
                Log.debug("level " + index + " playlist:\n" + string);
                var frags : Vector.<Fragment> = Manifest.getFragments(string, url);
                // set fragment and update sequence number range
                _levels[index].updateFragments(frags);
                _levels[index].targetduration = Manifest.getTargetDuration(string);
                _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYLIST_DURATION_UPDATED, _levels[index].duration));
            }

            // Check whether the stream is live or not finished yet
            if (Manifest.hasEndlist(string)) {
                _type = HLSTypes.VOD;
            } else {
                _type = HLSTypes.LIVE;
                var timeout : Number = Math.max(100, _reload_playlists_timer + 1000 * _levels[index].averageduration - getTimer());
                Log.debug("Level " + index + " Live Playlist parsing finished: reload in " + timeout.toFixed(0) + " ms");
                _timeoutID = setTimeout(_loadActiveLevelPlaylist, timeout);
            }
            if (!_canStart) {
                _canStart = (_levels[index].fragments.length > 0);
                if (_canStart) {
                    Log.debug("first level filled with at least 1 fragment, notify event");
                    _hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_LOADED, _levels));
                }
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_UPDATED, index));
            _load_in_progress = false;
        };

        /** Parse First Level Playlist **/
        private function _parseManifest(string : String) : void {
            // Check for M3U8 playlist or manifest.
            if (string.indexOf(Manifest.HEADER) == 0) {
                // 1 level playlist, create unique level and parse playlist
                if (string.indexOf(Manifest.FRAGMENT) > 0) {
                    var level : Level = new Level();
                    level.url = _url;
                    _levels.push(level);
                    Log.debug("1 Level Playlist, load it");
                    _parseLevelPlaylist(string, _url, 0);
                } else if (string.indexOf(Manifest.LEVEL) > 0) {
                    Log.debug("adaptive playlist:\n" + string);
                    // adaptative playlist, extract levels from playlist, get them and parse them
                    _levels = Manifest.extractLevels(string, _url);
                    _loadActiveLevelPlaylist();
                    if (string.indexOf(Manifest.ALTERNATE_AUDIO) > 0) {
                        Log.debug("alternate audio level found");
                        // parse alternate audio tracks
                        var altAudiolevels : Vector.<AltAudioTrack> = Manifest.extractAltAudioTracks(string, _url);
                        _hls.dispatchEvent(new HLSEvent(HLSEvent.ALT_AUDIO_TRACKS_LIST_CHANGE, altAudiolevels));
                    }
                }
            } else {
                var message : String = "Manifest is not a valid M3U8 file" + _url;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, message));
            }
        };

        /** load/reload active M3U8 playlist **/
        private function _loadActiveLevelPlaylist() : void {
            _load_in_progress = true;
            _reload_playlists_timer = getTimer();
            // load active M3U8 playlist only
            new Manifest().loadPlaylist(_levels[_current_level].url, _parseLevelPlaylist, _errorHandler, _current_level, _type, _flushLiveURLCache);
        };

        /** When level switch occurs, assess the need of (re)loading new level playlist **/
        private function _levelSwitchHandler(event : HLSEvent) : void {
            if (_current_level != event.level) {
                _current_level = event.level;
                Log.debug("switch to level " + _current_level);
                if (_type == HLSTypes.LIVE || _levels[_current_level].fragments.length == 0) {
                    Log.debug("(re)load Playlist");
                    clearTimeout(_timeoutID);
                    _timeoutID = setTimeout(_loadActiveLevelPlaylist, 0);
                }
            }
        };

        /** When the framework idles out, reloading is cancelled. **/
        private function _stateHandler(event : HLSEvent) : void {
            if (event.state == HLSStates.IDLE) {
                clearTimeout(_timeoutID);
            }
        };

        public function set flushLiveURLCache(val : Boolean) : void {
            _flushLiveURLCache = val;
        }

        public function get flushLiveURLCache() : Boolean {
            return _flushLiveURLCache;
        }
    }
}

move methods to private

move methods to private


package aerys.minko.scene.node
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.scene.SceneIterator;
	import aerys.minko.type.Signal;
	import aerys.minko.type.Sort;
	import aerys.minko.type.loader.ILoader;
	import aerys.minko.type.loader.SceneLoader;
	import aerys.minko.type.loader.parser.ParserOptions;
	import aerys.minko.type.math.Ray;

	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.getQualifiedClassName;

	use namespace minko_scene;

	/**
	 * Group objects can contain other scene nodes and applies a 3D
	 * transformation to its descendants.
	 *
	 * @author Jean-Marc Le Roux
	 */
	public class Group extends AbstractVisibleSceneNode
	{
		minko_scene var _children		: Vector.<ISceneNode>;
		minko_scene var _numChildren	: uint;

		private var _numDescendants		: uint;

		private var _descendantAdded	: Signal;
		private var _descendantRemoved	: Signal;

		/**
		 * The number of children of the Group.
		 */
		public function get numChildren() : uint
		{
			return _numChildren;
		}

		/**
		 * The number of descendants of the Group.
		 *
		 * @return
		 *
		 */
		public function get numDescendants() : uint
		{
			return _numDescendants;
		}

		/**
		 * The signal executed when a descendant is added to the Group.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
newParent : Group, the Group the descendant was actually added to

		 *
descendant : ISceneNode, the descendant that was just added

		 *

		 *
		 * @return
		 *
		 */
		public function get descendantAdded() : Signal
		{
			return _descendantAdded
		}

		/**
		 * The signal executed when a descendant is removed from the Group.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
oldParent : Group, the Group the descendant was actually removed from

		 *
descendant : ISceneNode, the descendant that was just removed

		 *

		 *
		 * @return
		 *
		 */
		public function get descendantRemoved() : Signal
		{
			return _descendantRemoved;
		}

		public function Group(...children)
		{
			super();

			initializeChildren(children);
		}

		override protected function initializeSignals() : void
		{
			super.initializeSignals();

            _descendantAdded = new Signal('Group.descendantAdded');
            _descendantRemoved = new Signal('Group.descendantRemoved');
		}

		override protected function initializeSignalHandlers() : void
		{
			super.initializeSignalHandlers();

			_descendantAdded.add(descendantAddedHandler);
			_descendantRemoved.add(descendantRemovedHandler);

			added.add(addedHandler);
			removed.add(removedHandler);
		}

		protected function initializeChildren(children : Array) : void
		{
			_children = new <ISceneNode>[];

			while (children.length == 1 && children[0] is Array)
				children = children[0];

			var childrenNum : uint = children.length;

			for (var childrenIndex : uint = 0; childrenIndex < childrenNum; ++childrenIndex)
				addChild(ISceneNode(children[childrenIndex]));
		}

		private function descendantAddedHandler(group : Group, child : ISceneNode) : void
		{
			if (group == this)
			{
				var childGroup : Group = child as Group;

				if (childGroup)
				{
					childGroup.descendantAdded.add(_descendantAdded.execute);
					childGroup.descendantRemoved.add(_descendantRemoved.execute);
				}
			}

			_numDescendants += (child is Group) ? (child as Group)._numDescendants + 1 : 1;
		}

		private function descendantRemovedHandler(group : Group, child : ISceneNode) : void
		{
			if (group == this)
			{
				var childGroup : Group = child as Group;

				if (childGroup)
				{
					childGroup.descendantAdded.remove(_descendantAdded.execute);
					childGroup.descendantRemoved.remove(_descendantRemoved.execute);
				}
			}

			_numDescendants -= (child is Group) ? (child as Group)._numDescendants + 1 : 1;
		}

        private function addedHandler(child : ISceneNode, ancestor : Group) : void
        {
            var numChildren : uint = this.numChildren;

            for (var childId : uint = 0; childId < numChildren; ++childId)
            {
                var child : ISceneNode = _children[childId];

                child.added.execute(child, ancestor);
            }
        }

        private function removedHandler(child : ISceneNode, ancestor : Group) : void
        {
            var numChildren : uint = this.numChildren;

            for (var childId : uint = 0; childId < numChildren; ++childId)
            {
                var child : ISceneNode = _children[childId];

                child.removed.execute(child, ancestor);
            }
        }

		/**
		 * Return true if the specified scene node is a child of the Group, false otherwise.
		 *
		 * @param scene
		 * @return
		 *
		 */
		public function contains(scene : ISceneNode) : Boolean
		{
			return getChildIndex(scene) >= 0;
		}

		/**
		 * Return the index of the speficied scene node or -1 if it is not in the Group.
		 *
		 * @param child
		 * @return
		 *
		 */
		public function getChildIndex(child : ISceneNode) : int
		{
			if (child == null)
				throw new Error('The \'child\' parameter cannot be null.');

			for (var i : int = 0; i < _numChildren; i++)
				if (_children[i] === child)
					return i;

			return -1;
		}

		public function getChildByName(name : String) : ISceneNode
		{
			for (var i : int = 0; i < _numChildren; i++)
				if (_children[i].name === name)
					return _children[i];

			return null;
		}

		/**
		 * Add a child to the group.
		 *
		 * @param	scene The child to add.
		 */
		public function addChild(node : ISceneNode) : Group
		{
			return addChildAt(node, _numChildren);
		}

		/**
		 * Add a child to the group at the specified position.
		 *
		 * @param node
		 * @param position
		 * @return
		 *
		 */
		public function addChildAt(node : ISceneNode, position : uint) : Group
		{
			if (!node)
				throw new Error('Parameter \'scene\' must not be null.');

			node.parent = this;

			for (var i : int = _numChildren - 1; i > position; --i)
				_children[i] = _children[int(i - 1)];

			_children[position] = node;

			return this;
		}

		/**
		 * Remove a child from the container.
		 *
		 * @param	myChild The child to remove.
		 * @return Whether the child was actually removed or not.
		 */
		public function removeChild(child : ISceneNode) : Group
		{
			return removeChildAt(getChildIndex(child));
		}

		public function removeChildAt(position : uint) : Group
		{
			if (position >= _numChildren)
				throw new Error('The scene node is not a child of the caller.');

			(_children[position] as ISceneNode).parent = null;

			return this;
		}

		/**
		 * Remove all the children.
		 *
		 * @return
		 *
		 */
		public function removeAllChildren() : Group
		{
			while (_numChildren)
				removeChildAt(0);

			return this;
		}

		/**
		 * Return the child at the specified position.
		 *
		 * @param position
		 * @return
		 *
		 */
		public function getChildAt(position : uint) : ISceneNode
		{
			return position < _numChildren ? _children[position] : null;
		}

		/**
		 * Returns the list of descendant scene nodes that have the specified type.
		 *
		 * @param type
		 * @param descendants
		 * @return
		 *
		 */
		public function getDescendantsByType(type 			: Class,
											 descendants 	: Vector.<ISceneNode> = null) : Vector.<ISceneNode>
		{
			descendants ||= new Vector.<ISceneNode>();

			for (var i : int = 0; i < _numChildren; ++i)
			{
				var child	: ISceneNode	= _children[i];
				var group 	: Group 		= child as Group;

				if (child is type)
					descendants.push(child);

				if (group)
					group.getDescendantsByType(type, descendants);
			}

			return descendants;
		}

		public function toString() : String
		{
			return '[' + getQualifiedClassName(this) + ' ' + name + ']';
		}

		/**
		 * Load the 3D scene corresponding to the specified URLRequest object
		 * and add it directly to the group.
		 *
		 * @param request
		 * @param options
		 * @return
		 *
		 */
		public function load(request	: URLRequest,
							 options	: ParserOptions	= null) : ILoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader	: SceneLoader	= new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.load(request);

			return loader;
		}

		/**
		 * Load the 3D scene corresponding to the specified Class object
		 * and add it directly to the group.
		 *
		 * @param classObject
		 * @param options
		 * @return
		 */
		public function loadClass(classObject	: Class,
								  options		: ParserOptions	= null) : SceneLoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader : SceneLoader = new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.loadClass(classObject);

			return loader;
		}

		/**
		 * Load the 3D scene corresponding to the specified ByteArray object
		 * and add it directly to the group.
		 *
		 * @param bytes
		 * @param options
		 * @return
		 *
		 */
		public function loadBytes(bytes		: ByteArray,
								  options	: ParserOptions	= null) : ILoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader	: SceneLoader	= new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.loadBytes(bytes);

			return loader;
		}

		/**
		 * Return the set of nodes matching the specified XPath query.
		 *
		 * @param xpath
		 * @return
		 *
		 */
		public function get(xpath : String) : SceneIterator
		{
			return new SceneIterator(xpath, new <ISceneNode>[this]);
		}

		private function loaderCompleteHandler(loader	: ILoader,
											   scene	: ISceneNode) : void
		{
			addChild(scene);
		}

		/**
		 * Return all the Mesh objects hit by the specified ray.
		 *
		 * @param ray
		 * @param maxDistance
		 * @return
		 *
		 */
		public function cast(ray : Ray, maxDistance : Number = Number.POSITIVE_INFINITY, tag : uint = 1) : SceneIterator
		{
			var meshes		: Vector.<ISceneNode> 	= getDescendantsByType(Mesh);
			var numMeshes	: uint					= meshes.length;
			var hit			: Array					= [];
			var depth		: Vector.<Number>		= new <Number>[];
			var numItems	: uint					= 0;

			for (var i : uint = 0; i < numMeshes; ++i)
			{
				var mesh 		: Mesh		= meshes[i] as Mesh;
				var hitDepth	: Number	= mesh.cast(ray, maxDistance, tag);

				if (hitDepth >= 0.0)
				{
					hit[numItems] = mesh;
					depth[numItems] = hitDepth;
					++numItems;
				}
			}

			if (numItems > 1)
				Sort.flashSort(depth, hit, numItems);

			return new SceneIterator(null, Vector.<ISceneNode>(hit));
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var clone : Group = new Group();

			clone.name = name;
			clone.transform.copyFrom(transform);

			for (var childId : uint = 0; childId < _numChildren; ++childId)
			{
				var child		: AbstractSceneNode = AbstractSceneNode(_children[childId]);
				var clonedChild	: AbstractSceneNode = AbstractSceneNode(child.cloneNode());

				clone.addChild(clonedChild);
			}

			return clone;
		}
	}
}

package aerys.minko.scene.node
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.scene.SceneIterator;
	import aerys.minko.type.Signal;
	import aerys.minko.type.Sort;
	import aerys.minko.type.loader.ILoader;
	import aerys.minko.type.loader.SceneLoader;
	import aerys.minko.type.loader.parser.ParserOptions;
	import aerys.minko.type.math.Ray;

	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.getQualifiedClassName;

	use namespace minko_scene;

	/**
	 * Group objects can contain other scene nodes and applies a 3D
	 * transformation to its descendants.
	 *
	 * @author Jean-Marc Le Roux
	 */
	public class Group extends AbstractVisibleSceneNode
	{
		minko_scene var _children		: Vector.<ISceneNode>;
		minko_scene var _numChildren	: uint;

		private var _numDescendants		: uint;

		private var _descendantAdded	: Signal;
		private var _descendantRemoved	: Signal;

		/**
		 * The number of children of the Group.
		 */
		public function get numChildren() : uint
		{
			return _numChildren;
		}

		/**
		 * The number of descendants of the Group.
		 *
		 * @return
		 *
		 */
		public function get numDescendants() : uint
		{
			return _numDescendants;
		}

		/**
		 * The signal executed when a descendant is added to the Group.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
newParent : Group, the Group the descendant was actually added to

		 *
descendant : ISceneNode, the descendant that was just added

		 *

		 *
		 * @return
		 *
		 */
		public function get descendantAdded() : Signal
		{
			return _descendantAdded
		}

		/**
		 * The signal executed when a descendant is removed from the Group.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
oldParent : Group, the Group the descendant was actually removed from

		 *
descendant : ISceneNode, the descendant that was just removed

		 *

		 *
		 * @return
		 *
		 */
		public function get descendantRemoved() : Signal
		{
			return _descendantRemoved;
		}

		public function Group(...children)
		{
			super();

			initializeChildren(children);
		}

		override protected function initializeSignals() : void
		{
			super.initializeSignals();

            _descendantAdded = new Signal('Group.descendantAdded');
            _descendantRemoved = new Signal('Group.descendantRemoved');
		}

		override protected function initializeSignalHandlers() : void
		{
			super.initializeSignalHandlers();

			_descendantAdded.add(descendantAddedHandler);
			_descendantRemoved.add(descendantRemovedHandler);

			added.add(addedHandler);
			removed.add(removedHandler);
		}

		protected function initializeChildren(children : Array) : void
		{
			_children = new <ISceneNode>[];

			while (children.length == 1 && children[0] is Array)
				children = children[0];

			var childrenNum : uint = children.length;

			for (var childrenIndex : uint = 0; childrenIndex < childrenNum; ++childrenIndex)
				addChild(ISceneNode(children[childrenIndex]));
		}

		private function descendantAddedHandler(group : Group, child : ISceneNode) : void
		{
			if (group == this)
			{
				var childGroup : Group = child as Group;

				if (childGroup)
				{
					childGroup.descendantAdded.add(_descendantAdded.execute);
					childGroup.descendantRemoved.add(_descendantRemoved.execute);
				}
			}

			_numDescendants += (child is Group) ? (child as Group)._numDescendants + 1 : 1;
		}

		private function descendantRemovedHandler(group : Group, child : ISceneNode) : void
		{
			if (group == this)
			{
				var childGroup : Group = child as Group;

				if (childGroup)
				{
					childGroup.descendantAdded.remove(_descendantAdded.execute);
					childGroup.descendantRemoved.remove(_descendantRemoved.execute);
				}
			}

			_numDescendants -= (child is Group) ? (child as Group)._numDescendants + 1 : 1;
		}

        private function addedHandler(child : ISceneNode, ancestor : Group) : void
        {
            var numChildren : uint = this.numChildren;

            for (var childId : uint = 0; childId < numChildren; ++childId)
            {
                var child : ISceneNode = _children[childId];

                child.added.execute(child, ancestor);
            }
        }

        private function removedHandler(child : ISceneNode, ancestor : Group) : void
        {
            var numChildren : uint = this.numChildren;

            for (var childId : uint = 0; childId < numChildren; ++childId)
            {
                var child : ISceneNode = _children[childId];

                child.removed.execute(child, ancestor);
            }
        }

		/**
		 * Return true if the specified scene node is a child of the Group, false otherwise.
		 *
		 * @param scene
		 * @return
		 *
		 */
		public function contains(scene : ISceneNode) : Boolean
		{
			return getChildIndex(scene) >= 0;
		}

		/**
		 * Return the index of the speficied scene node or -1 if it is not in the Group.
		 *
		 * @param child
		 * @return
		 *
		 */
		public function getChildIndex(child : ISceneNode) : int
		{
			if (child == null)
				throw new Error('The \'child\' parameter cannot be null.');

			for (var i : int = 0; i < _numChildren; i++)
				if (_children[i] === child)
					return i;

			return -1;
		}

		public function getChildByName(name : String) : ISceneNode
		{
			for (var i : int = 0; i < _numChildren; i++)
				if (_children[i].name === name)
					return _children[i];

			return null;
		}

		/**
		 * Add a child to the group.
		 *
		 * @param	scene The child to add.
		 */
		public function addChild(node : ISceneNode) : Group
		{
			return addChildAt(node, _numChildren);
		}

		/**
		 * Add a child to the group at the specified position.
		 *
		 * @param node
		 * @param position
		 * @return
		 *
		 */
		public function addChildAt(node : ISceneNode, position : uint) : Group
		{
			if (!node)
				throw new Error('Parameter \'node\' must not be null.');

			node.parent = this;

			for (var i : int = _numChildren - 1; i > position; --i)
				_children[i] = _children[int(i - 1)];

			_children[position] = node;

			return this;
		}

		/**
		 * Remove a child from the container.
		 *
		 * @param	myChild The child to remove.
		 * @return Whether the child was actually removed or not.
		 */
		public function removeChild(child : ISceneNode) : Group
		{
			return removeChildAt(getChildIndex(child));
		}

		public function removeChildAt(position : uint) : Group
		{
			if (position >= _numChildren)
				throw new Error('The scene node is not a child of the caller.');

			(_children[position] as ISceneNode).parent = null;

			return this;
		}

		/**
		 * Remove all the children.
		 *
		 * @return
		 *
		 */
		public function removeAllChildren() : Group
		{
			while (_numChildren)
				removeChildAt(0);

			return this;
		}

		/**
		 * Return the child at the specified position.
		 *
		 * @param position
		 * @return
		 *
		 */
		public function getChildAt(position : uint) : ISceneNode
		{
			return position < _numChildren ? _children[position] : null;
		}

		/**
		 * Returns the list of descendant scene nodes that have the specified type.
		 *
		 * @param type
		 * @param descendants
		 * @return
		 *
		 */
		public function getDescendantsByType(type 			: Class,
											 descendants 	: Vector.<ISceneNode> = null) : Vector.<ISceneNode>
		{
			descendants ||= new Vector.<ISceneNode>();

			for (var i : int = 0; i < _numChildren; ++i)
			{
				var child	: ISceneNode	= _children[i];
				var group 	: Group 		= child as Group;

				if (child is type)
					descendants.push(child);

				if (group)
					group.getDescendantsByType(type, descendants);
			}

			return descendants;
		}

		public function toString() : String
		{
			return '[' + getQualifiedClassName(this) + ' ' + name + ']';
		}

		/**
		 * Load the 3D scene corresponding to the specified URLRequest object
		 * and add it directly to the group.
		 *
		 * @param request
		 * @param options
		 * @return
		 *
		 */
		public function load(request	: URLRequest,
							 options	: ParserOptions	= null) : ILoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader	: SceneLoader	= new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.load(request);

			return loader;
		}

		/**
		 * Load the 3D scene corresponding to the specified Class object
		 * and add it directly to the group.
		 *
		 * @param classObject
		 * @param options
		 * @return
		 */
		public function loadClass(classObject	: Class,
								  options		: ParserOptions	= null) : SceneLoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader : SceneLoader = new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.loadClass(classObject);

			return loader;
		}

		/**
		 * Load the 3D scene corresponding to the specified ByteArray object
		 * and add it directly to the group.
		 *
		 * @param bytes
		 * @param options
		 * @return
		 *
		 */
		public function loadBytes(bytes		: ByteArray,
								  options	: ParserOptions	= null) : ILoader
		{
			if (options != null && scene != null && options.assets == null)
				options.assets = scene.assets;

			var loader	: SceneLoader	= new SceneLoader(options);

			loader.complete.add(loaderCompleteHandler);
			loader.loadBytes(bytes);

			return loader;
		}

		/**
		 * Return the set of nodes matching the specified XPath query.
		 *
		 * @param xpath
		 * @return
		 *
		 */
		public function get(xpath : String) : SceneIterator
		{
			return new SceneIterator(xpath, new <ISceneNode>[this]);
		}

		private function loaderCompleteHandler(loader	: ILoader,
											   scene	: ISceneNode) : void
		{
			addChild(scene);
		}

		/**
		 * Return all the Mesh objects hit by the specified ray.
		 *
		 * @param ray
		 * @param maxDistance
		 * @return
		 *
		 */
		public function cast(ray : Ray, maxDistance : Number = Number.POSITIVE_INFINITY, tag : uint = 1) : SceneIterator
		{
			var meshes		: Vector.<ISceneNode> 	= getDescendantsByType(Mesh);
			var numMeshes	: uint					= meshes.length;
			var hit			: Array					= [];
			var depth		: Vector.<Number>		= new <Number>[];
			var numItems	: uint					= 0;

			for (var i : uint = 0; i < numMeshes; ++i)
			{
				var mesh 		: Mesh		= meshes[i] as Mesh;
				var hitDepth	: Number	= mesh.cast(ray, maxDistance, tag);

				if (hitDepth >= 0.0)
				{
					hit[numItems] = mesh;
					depth[numItems] = hitDepth;
					++numItems;
				}
			}

			if (numItems > 1)
				Sort.flashSort(depth, hit, numItems);

			return new SceneIterator(null, Vector.<ISceneNode>(hit));
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var clone : Group = new Group();

			clone.name = name;
			clone.transform.copyFrom(transform);

			for (var childId : uint = 0; childId < _numChildren; ++childId)
			{
				var child		: AbstractSceneNode = AbstractSceneNode(_children[childId]);
				var clonedChild	: AbstractSceneNode = AbstractSceneNode(child.cloneNode());

				clone.addChild(clonedChild);
			}

			return clone;
		}
	}
}

fix typo in error message

fix typo in error message


package aerys.minko.scene.controller
{
	import aerys.minko.ns.minko_scene;
	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;

    use namespace minko_scene;

	public class AbstractScriptController 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;

        private var _targetsListLocked  : Boolean;
        private var _targetsToAdd       : Vector.<ISceneNode>;
        private var _targetsToRemove    : Vector.<ISceneNode>;

		private var _updateRate			: Number;

        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;
		}

		protected function get updateRate() : Number
		{
			return _updateRate;
		}
		protected function set updateRate(value : Number) : void
		{
			_updateRate = value;
		}

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

            initialize();
		}

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

		override protected function targetAddedToScene(target	: ISceneNode,
													   scene	: Scene) : void
		{
            super.targetAddedToScene(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 targetRemovedFromScene(target 	: ISceneNode,
														   scene 	: Scene) : void
        {
            super.targetAddedToScene(target, scene);

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

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

			if (_updateRate != 0. && deltaTime < 1000. / _updateRate)
				return;

			_viewport = viewport;
			_lastTime = time;

            lockTargetsList();
            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();
            unlockTargetsList();
		}

        /**
         * 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
		}

        private function lockTargetsList() : void
        {
            _targetsListLocked = true;
        }

        private function unlockTargetsList() : void
        {
            _targetsListLocked = false;

            if (_targetsToAdd)
            {
                var numTargetsToAdd : uint = _targetsToAdd.length;
                for (var i : uint = 0; i < numTargetsToAdd; ++i)
                    addTarget(_targetsToAdd[i]);
                _targetsToAdd = null;
            }

            if (_targetsToRemove)
            {
                var numTargetsToRemove : uint = _targetsToRemove.length;
                for (var j : uint = 0; j < numTargetsToRemove; ++j)
                    removeTarget(_targetsToRemove[j]);
                _targetsToRemove = null;
            }
        }

        override minko_scene function addTarget(target : ISceneNode) : void
        {
            if (_targetsListLocked)
            {
                if (_targetsToAdd)
                    _targetsToAdd.push(target);
                else
                    _targetsToAdd = new <ISceneNode>[target];
            }
            else
                super.addTarget(target);
        }

        override minko_scene function removeTarget(target : ISceneNode) : void
        {
            if (_targetsListLocked)
            {
                if (_targetsToRemove)
                    _targetsToRemove.push(target);
                else
                    _targetsToRemove = new <ISceneNode>[target];
            }
            else
                super.removeTarget(target);
        }
	}
}

package aerys.minko.scene.controller
{
	import aerys.minko.ns.minko_scene;
	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;

    use namespace minko_scene;

	public class AbstractScriptController 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;

        private var _targetsListLocked  : Boolean;
        private var _targetsToAdd       : Vector.<ISceneNode>;
        private var _targetsToRemove    : Vector.<ISceneNode>;

		private var _updateRate			: Number	= 0.;

        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;
		}

		protected function get updateRate() : Number
		{
			return _updateRate;
		}
		protected function set updateRate(value : Number) : void
		{
			_updateRate = value;
		}

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

            initialize();
		}

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

		override protected function targetAddedToScene(target	: ISceneNode,
													   scene	: Scene) : void
		{
            super.targetAddedToScene(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 targetRemovedFromScene(target 	: ISceneNode,
														   scene 	: Scene) : void
        {
            super.targetAddedToScene(target, scene);

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

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

			if (_updateRate != 0. && deltaTime < 1000. / _updateRate)
				return;

			_viewport = viewport;
			_lastTime = time;

            lockTargetsList();
            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();
            unlockTargetsList();
		}

        /**
         * 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
		}

        private function lockTargetsList() : void
        {
            _targetsListLocked = true;
        }

        private function unlockTargetsList() : void
        {
            _targetsListLocked = false;

            if (_targetsToAdd)
            {
                var numTargetsToAdd : uint = _targetsToAdd.length;
                for (var i : uint = 0; i < numTargetsToAdd; ++i)
                    addTarget(_targetsToAdd[i]);
                _targetsToAdd = null;
            }

            if (_targetsToRemove)
            {
                var numTargetsToRemove : uint = _targetsToRemove.length;
                for (var j : uint = 0; j < numTargetsToRemove; ++j)
                    removeTarget(_targetsToRemove[j]);
                _targetsToRemove = null;
            }
        }

        override minko_scene function addTarget(target : ISceneNode) : void
        {
            if (_targetsListLocked)
            {
                if (_targetsToAdd)
                    _targetsToAdd.push(target);
                else
                    _targetsToAdd = new <ISceneNode>[target];
            }
            else
                super.addTarget(target);
        }

        override minko_scene function removeTarget(target : ISceneNode) : void
        {
            if (_targetsListLocked)
            {
                if (_targetsToRemove)
                    _targetsToRemove.push(target);
                else
                    _targetsToRemove = new <ISceneNode>[target];
            }
            else
                super.removeTarget(target);
        }
	}
}

Fix invalid NaN comparison.

Fix invalid NaN comparison.


package aerys.minko.scene.node
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.render.geometry.Geometry;
	import aerys.minko.render.material.Material;
	import aerys.minko.render.material.basic.BasicMaterial;
	import aerys.minko.scene.controller.mesh.VisibilityController;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.binding.DataProvider;
	import aerys.minko.type.enum.DataProviderUsage;
	import aerys.minko.type.math.Ray;

	use namespace minko_scene;

	/**
	 * Mesh objects are a visible instance of a Geometry rendered using a specific
	 * Effect with specific rendering properties.
	 *
	 *

	 * Those rendering properties are stored in a DataBindings object so they can
	 * be directly used by the shaders in the rendering API.
	 *

	 *
	 * @author Jean-Marc Le Roux
	 *
	 */
	public class Mesh extends AbstractSceneNode
	{
		public static const DEFAULT_MATERIAL	: Material	= new BasicMaterial();

		private var _geometry			: Geometry;
		private var _properties			: DataProvider;
		private var _material			: Material;
		private var _bindings			: DataBindings;

		private var _visibility			: VisibilityController;

		private var _frame				: uint;

		private var _cloned				: Signal;
		private var _materialChanged	: Signal;
		private var _frameChanged		: Signal;
		private var _geometryChanged	: Signal;

		/**
		 * A DataProvider object already bound to the Mesh bindings.
		 *
		 *

		 * // set the "diffuseColor" property to 0x0000ffff
		 * mesh.properties.diffuseColor = 0x0000ffff;
		 *
		 * // animate the "diffuseColor" property
		 * mesh.addController(
		 *   new AnimationController(
		 * 	  new <ITimeline>[new ColorTimeline(
		 * 	    "dataProvider.diffuseColor",
		 * 	    5000,
		 * 	    new <uint>[0xffffffff, 0xffffff00, 0xffffffff]
		 *    )]
		 *   )
		 * );
		 *

		 *
		 * @return
		 *
		 */
		public function get properties() : DataProvider
		{
			return _properties;
		}

		public function set properties(value : DataProvider) : void
		{
			if (_properties != value)
			{
				if (_properties)
					_bindings.removeProvider(_properties);

				_properties = value;

				if (value)
					_bindings.addProvider(value);
			}
		}

		public function get material() : Material
		{
			return _material;
		}
		public function set material(value : Material) : void
		{
			if (_material != value)
			{
				if (_material)
					_bindings.removeProvider(_material);

				_material = value;

				if (value)
					_bindings.addProvider(value);
			}
		}

		/**
		 * The rendering properties provided to the shaders to customize
		 * how the mesh will appear on screen.
		 *
		 * @return
		 *
		 */
		public function get bindings() : DataBindings
		{
			return _bindings;
		}

		/**
		 * The Geometry of the mesh.
		 * @return
		 *
		 */
		public function get geometry() : Geometry
		{
			return _geometry;
		}

		public function set geometry(value : Geometry) : void
		{
			if (_geometry != value)
			{
				var oldGeometry	: Geometry = _geometry;

				_geometry = value;
				_geometryChanged.execute(this, oldGeometry, value);
			}
		}

		/**
		 * Whether the mesh is visible or not.
		 *
		 * @return
		 *
		 */
		public function get visible() : Boolean
		{
			return _visibility.visible;
		}
		public function set visible(value : Boolean) : void
		{
			_visibility.visible = value;
		}

		public function get frustumCulling() : uint
		{
			return _visibility.frustumCulling;
		}
		public function set frustumCulling(value : uint) : void
		{
			_visibility.frustumCulling = value;
		}

		public function get cloned() : Signal
		{
			return _cloned;
		}

		public function get materialChanged() : Signal
		{
			return _materialChanged;
		}

		public function get frameChanged() : Signal
		{
			return _frameChanged;
		}

		public function get geometryChanged() : Signal
		{
			return _geometryChanged;
		}

		public function get frame() : uint
		{
			return _frame;
		}
		public function set frame(value : uint) : void
		{
			if (_frame != value)
			{
				var oldFrame : uint = _frame;

				_frame = value;
				_frameChanged.execute(this, oldFrame, value);
			}
		}

		public function Mesh(geometry	: Geometry	= null,
							 material	: Material	= null,
							 name		: String	= null)
		{
			super();

			initialize(geometry, material, name);
		}

		private function initialize(geometry	: Geometry,
									material	: Material,
									name		: String) : void
		{
			if (name)
				this.name = name;

			_cloned = new Signal('Mesh.clones');
			_materialChanged = new Signal('Mesh.materialChanged');
			_frameChanged = new Signal('Mesh.frameChanged');
			_geometryChanged = new Signal('Mesh.geometryChanged');

			_bindings = new DataBindings(this);
			this.properties = new DataProvider(properties, 'meshProperties', DataProviderUsage.EXCLUSIVE);

			_geometry = geometry;
			this.material = material || DEFAULT_MATERIAL;

			_visibility = new VisibilityController();
//			_visibility.frustumCulling = FrustumCulling.BOX ^ FrustumCulling.TOP_BOX;
			addController(_visibility);
		}

		public function cast(ray : Ray, maxDistance : Number = Number.POSITIVE_INFINITY) : Number
		{
			return _geometry.boundingBox.testRay(
				ray,
				worldToLocal,
				maxDistance
			);
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var clone : Mesh = new Mesh();

			clone.name 		= name;
			clone.geometry 	= _geometry;

			clone.properties = DataProvider(_properties.clone());
			clone._bindings.copySharedProvidersFrom(_bindings);

			clone.transform.copyFrom(transform);
			clone.material = _material;

			this.cloned.execute(this, clone);

			return clone;
		}

		override protected function addedToSceneHandler(child : ISceneNode, scene : Scene) : void
		{
			super.addedToSceneHandler(child, scene);

			if (child === this)
				_bindings.addProvider(transformData);
		}

		override protected function removedFromSceneHandler(child : ISceneNode, scene : Scene) : void
		{
			super.removedFromSceneHandler(child, scene);

			if (child === this)
				_bindings.removeProvider(transformData);
		}
	}
}

package aerys.minko.scene.node
{
	import aerys.minko.ns.minko_scene;
	import aerys.minko.render.geometry.Geometry;
	import aerys.minko.render.material.Material;
	import aerys.minko.render.material.basic.BasicMaterial;
	import aerys.minko.scene.controller.mesh.VisibilityController;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.DataBindings;
	import aerys.minko.type.binding.DataProvider;
	import aerys.minko.type.enum.DataProviderUsage;
	import aerys.minko.type.math.Ray;

	use namespace minko_scene;

	/**
	 * Mesh objects are a visible instance of a Geometry rendered using a specific
	 * Effect with specific rendering properties.
	 *
	 *

	 * Those rendering properties are stored in a DataBindings object so they can
	 * be directly used by the shaders in the rendering API.
	 *

	 *
	 * @author Jean-Marc Le Roux
	 *
	 */
	public class Mesh extends AbstractSceneNode
	{
		public static const DEFAULT_MATERIAL	: Material	= new BasicMaterial();

		private var _geometry			: Geometry;
		private var _properties			: DataProvider;
		private var _material			: Material;
		private var _bindings			: DataBindings;

		private var _visibility			: VisibilityController;

		private var _frame				: uint;

		private var _cloned				: Signal;
		private var _materialChanged	: Signal;
		private var _frameChanged		: Signal;
		private var _geometryChanged	: Signal;

		/**
		 * A DataProvider object already bound to the Mesh bindings.
		 *
		 *

		 * // set the "diffuseColor" property to 0x0000ffff
		 * mesh.properties.diffuseColor = 0x0000ffff;
		 *
		 * // animate the "diffuseColor" property
		 * mesh.addController(
		 *   new AnimationController(
		 * 	  new <ITimeline>[new ColorTimeline(
		 * 	    "dataProvider.diffuseColor",
		 * 	    5000,
		 * 	    new <uint>[0xffffffff, 0xffffff00, 0xffffffff]
		 *    )]
		 *   )
		 * );
		 *

		 *
		 * @return
		 *
		 */
		public function get properties() : DataProvider
		{
			return _properties;
		}

		public function set properties(value : DataProvider) : void
		{
			if (_properties != value)
			{
				if (_properties)
					_bindings.removeProvider(_properties);

				_properties = value;

				if (value)
					_bindings.addProvider(value);
			}
		}

		public function get material() : Material
		{
			return _material;
		}
		public function set material(value : Material) : void
		{
			if (_material != value)
			{
				if (_material)
					_bindings.removeProvider(_material);

				_material = value;

				if (value)
					_bindings.addProvider(value);
			}
		}

		/**
		 * The rendering properties provided to the shaders to customize
		 * how the mesh will appear on screen.
		 *
		 * @return
		 *
		 */
		public function get bindings() : DataBindings
		{
			return _bindings;
		}

		/**
		 * The Geometry of the mesh.
		 * @return
		 *
		 */
		public function get geometry() : Geometry
		{
			return _geometry;
		}

		public function set geometry(value : Geometry) : void
		{
			if (_geometry != value)
			{
				var oldGeometry	: Geometry = _geometry;

				if (oldGeometry)
					oldGeometry.changed.remove(geometryChangedHandler);

				_geometry = value;

				if (value)
					_geometry.changed.add(geometryChangedHandler);

				_geometryChanged.execute(this, oldGeometry, value);
			}
		}

		/**
		 * Whether the mesh is visible or not.
		 *
		 * @return
		 *
		 */
		public function get visible() : Boolean
		{
			return _visibility.visible;
		}
		public function set visible(value : Boolean) : void
		{
			_visibility.visible = value;
		}

		public function get frustumCulling() : uint
		{
			return _visibility.frustumCulling;
		}
		public function set frustumCulling(value : uint) : void
		{
			_visibility.frustumCulling = value;
		}

		public function get cloned() : Signal
		{
			return _cloned;
		}

		public function get materialChanged() : Signal
		{
			return _materialChanged;
		}

		public function get frameChanged() : Signal
		{
			return _frameChanged;
		}

		public function get geometryChanged() : Signal
		{
			return _geometryChanged;
		}

		public function get frame() : uint
		{
			return _frame;
		}
		public function set frame(value : uint) : void
		{
			if (_frame != value)
			{
				var oldFrame : uint = _frame;

				_frame = value;
				_frameChanged.execute(this, oldFrame, value);
			}
		}

		public function Mesh(geometry	: Geometry	= null,
							 material	: Material	= null,
							 name		: String	= null)
		{
			super();

			initialize(geometry, material, name);
		}

		private function initialize(geometry	: Geometry,
									material	: Material,
									name		: String) : void
		{
			if (name)
				this.name = name;

			_cloned = new Signal('Mesh.clones');
			_materialChanged = new Signal('Mesh.materialChanged');
			_frameChanged = new Signal('Mesh.frameChanged');
			_geometryChanged = new Signal('Mesh.geometryChanged');

			_bindings = new DataBindings(this);
			this.properties = new DataProvider(properties, 'meshProperties', DataProviderUsage.EXCLUSIVE);

			this.geometry = geometry;
			this.material = material || DEFAULT_MATERIAL;

			_visibility = new VisibilityController();
//			_visibility.frustumCulling = FrustumCulling.BOX ^ FrustumCulling.TOP_BOX;
			addController(_visibility);
		}

		public function cast(ray : Ray, maxDistance : Number = Number.POSITIVE_INFINITY) : Number
		{
			return _geometry.boundingBox.testRay(
				ray,
				worldToLocal,
				maxDistance
			);
		}

		override minko_scene function cloneNode() : AbstractSceneNode
		{
			var clone : Mesh = new Mesh();

			clone.name 		= name;
			clone.geometry 	= _geometry;

			clone.properties = DataProvider(_properties.clone());
			clone._bindings.copySharedProvidersFrom(_bindings);

			clone.transform.copyFrom(transform);
			clone.material = _material;

			this.cloned.execute(this, clone);

			return clone;
		}

		override protected function addedToSceneHandler(child : ISceneNode, scene : Scene) : void
		{
			super.addedToSceneHandler(child, scene);

			if (child === this)
				_bindings.addProvider(transformData);
		}

		override protected function removedFromSceneHandler(child : ISceneNode, scene : Scene) : void
		{
			super.removedFromSceneHandler(child, scene);

			if (child === this)
				_bindings.removeProvider(transformData);
		}

		private function geometryChangedHandler(geometry : Geometry) : void
		{
			_geometryChanged.execute(this, _geometry, _geometry);
		}
	}
}

update Mesh.geometry setter to listen to the Geometry.changed signal and execute Mesh.geometryChanged when it's triggered