package aerys.minko.render.material.phong
{
	import aerys.minko.render.DataBindingsProxy;
	import aerys.minko.render.Effect;
	import aerys.minko.render.RenderTarget;
	import aerys.minko.render.material.phong.multipass.PhongAdditionalShader;
	import aerys.minko.render.material.phong.multipass.PhongAmbientShader;
	import aerys.minko.render.material.phong.multipass.PhongEmissiveShader;
	import aerys.minko.render.material.phong.multipass.ZPrepassShader;
	import aerys.minko.render.material.phong.shadow.ExponentialShadowMapShader;
	import aerys.minko.render.material.phong.shadow.PCFShadowMapShader;
	import aerys.minko.render.material.phong.shadow.ParaboloidShadowMapShader;
	import aerys.minko.render.material.phong.shadow.VarianceShadowMapShader;
	import aerys.minko.render.resource.texture.CubeTextureResource;
	import aerys.minko.render.resource.texture.ITextureResource;
	import aerys.minko.render.resource.texture.TextureResource;
	import aerys.minko.render.shader.Shader;
	import aerys.minko.scene.data.LightDataProvider;
	import aerys.minko.scene.node.light.AmbientLight;
	import aerys.minko.scene.node.light.PointLight;
	import aerys.minko.type.enum.ShadowMappingQuality;
	import aerys.minko.type.enum.ShadowMappingType;

    /**
     *
The PhongEffect using the Phong lighting model to render the geometry according to
     * the lighting setup of the scene. It supports an infinite number of lights/projected
     * shadows and will automatically switch between singlepass and multipass rendering
     * in order to give the best performances whenever possible.

     *
     *
Because of the Stage3D restrictions regarding the number of shader operations or
     * the number of available registers, the number of lights might be to big to allow them
     * to be rendered in a single pass. In this situation, the PhongEffect will automatically
     * fallback and use multipass rendering.

     *
     *
Multipass rendering is done as follow:

     *

     *
The "base" pass renders objects with one per-pixel directional lights with shadows,
     * the lightmap and the ambient/emissive lighting.

     *
Each "additional" pass (one per light) will render a single light with shadows and
     * blend it using additive blending.

     *

     *
     * The singlepass rendering will mimic this behavior in order to get preserve consistency.
     *
     * @author Jean-Marc Le Roux
     *
     */
	public class PhongEffect extends Effect
	{
        private var _useRenderToTexture : Boolean;
        private var _singlePassShader   : Shader;
        private var _emissiveShader     : Shader;

        private var _targets            : Array;

		public function PhongEffect(useRenderToTexture   : Boolean   = false,
                                    singlePassShader     : Shader    = null,
                                    emissiveShader           : Shader    = null)
		{
            super();

            _useRenderToTexture = useRenderToTexture;
            _singlePassShader = singlePassShader || new PhongSinglePassShader(null, 0);
            _emissiveShader = emissiveShader || new PhongEmissiveShader(null, null, .25);

            _targets = [];
		}

        override protected function initializePasses(sceneBindings	: DataBindingsProxy,
                                                     meshBindings	: DataBindingsProxy) : Vector.<Shader>
        {
            var passes : Vector.<Shader>    = super.initializePasses(sceneBindings, meshBindings);

            for (var lightId : uint = 0;
                lightPropertyExists(sceneBindings, lightId, 'enabled');
                ++lightId)
            {
                if (lightPropertyExists(sceneBindings, lightId, 'shadowCastingType'))
                {
                    var lightType			: uint	= getLightProperty(sceneBindings, lightId, 'type');
                    var shadowMappingType 	: uint	= getLightProperty(
                        sceneBindings, lightId, 'shadowCastingType'
                    );

                    switch (shadowMappingType)
                    {
                        case ShadowMappingType.PCF:
                            if (lightType == PointLight.LIGHT_TYPE)
                                pushCubeShadowMappingPass(sceneBindings, lightId, passes);
                            else
                                pushPCFShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.DUAL_PARABOLOID:
                            pushDualParaboloidShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.VARIANCE:
                            pushVarianceShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.EXPONENTIAL:
                            pushExponentialShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                    }
                }
            }

            passes.push(_singlePassShader);

            return passes;
        }

        override protected function initializeFallbackPasses(sceneBindings  : DataBindingsProxy,
                                                             meshBindings   : DataBindingsProxy) : Vector.<Shader>
        {
            var passes              : Vector.<Shader>   = new <Shader>[];
            var discardDirectional  : Boolean           = true;
            var ambientEnabled      : Boolean           = meshBindings.propertyExists('lightmap');
            var renderTarget        : RenderTarget      = null;

            if (_useRenderToTexture)
            {
                var accumulatorSize : uint  = sceneBindings.getProperty('viewportWidth');

                accumulatorSize = 1 << Math.ceil(Math.log(accumulatorSize) * Math.LOG2E);

                renderTarget = _targets[accumulatorSize] = new RenderTarget(
                    accumulatorSize, accumulatorSize,
                    new TextureResource(accumulatorSize, accumulatorSize)
                );
            }

			for (var lightId : uint = 0;
                lightPropertyExists(sceneBindings, lightId, 'enabled');
                ++lightId)
			{
                var lightType : uint = getLightProperty(sceneBindings, lightId, 'type');

                if (lightType == AmbientLight.LIGHT_TYPE)
                {
                    ambientEnabled = true;
                    continue;
                }

                if (getLightProperty(sceneBindings, lightId, 'diffuseEnabled'))
                    passes.push(
                        new PhongAdditionalShader(lightId, true, false, renderTarget, .5)
                    );

                if (getLightProperty(sceneBindings, lightId, 'specularEnabled'))
                    passes.push(
                        new PhongAdditionalShader(lightId, false, true, null, 0.)
                    );

				if (lightPropertyExists(sceneBindings, lightId, 'shadowCastingType'))
				{
					var shadowMappingType 	: uint	= getLightProperty(
						sceneBindings, lightId, 'shadowCastingType'
					);

					switch (shadowMappingType)
					{
						case ShadowMappingType.PCF:
							if (lightType == PointLight.LIGHT_TYPE)
								pushCubeShadowMappingPass(sceneBindings, lightId, passes);
							else
								pushPCFShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.DUAL_PARABOLOID:
							pushDualParaboloidShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.VARIANCE:
							pushVarianceShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.EXPONENTIAL:
							pushExponentialShadowMappingPass(sceneBindings, lightId, passes);
							break ;
					}
				}
			}

            if (ambientEnabled)
                passes.push(new PhongAmbientShader(renderTarget, .75));

            passes.push(new ZPrepassShader(renderTarget, 1));
            passes.push(new PhongEmissiveShader(
                renderTarget ? renderTarget.textureResource : null, null, .25
            ));

            return passes;
		}

		private function pushPCFShadowMappingPass(sceneBindings	: DataBindingsProxy,
                                                  lightId 		: uint,
                                                  passes 		: Vector.<Shader>) : void
		{
			var textureResource : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMap'
			);
			var renderTarget	: RenderTarget		= new RenderTarget(
				textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
			);

			passes.push(new PCFShadowMapShader(lightId, lightId + 1, renderTarget));
		}

		private function pushDualParaboloidShadowMappingPass(sceneBindings	: DataBindingsProxy,
															 lightId 		: uint,
															 passes 		: Vector.<Shader>) : void
		{
			var frontTextureResource : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMapFront'
			);
			var backTextureResource	 : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMapBack'
			);
			var size				 : uint				= frontTextureResource.width;
			var frontRenderTarget	 : RenderTarget		= new RenderTarget(
				size, size, frontTextureResource, 0, 0xffffffff
			);
			var backRenderTarget	 : RenderTarget		= new RenderTarget(
				size, size, backTextureResource, 0, 0xffffffff
			);

			passes.push(
				new ParaboloidShadowMapShader(lightId, true, lightId + 0.5, frontRenderTarget),
				new ParaboloidShadowMapShader(lightId, false, lightId + 1, backRenderTarget)
			);
		}

		private function pushCubeShadowMappingPass(sceneBindings	: DataBindingsProxy,
												   lightId 			: uint,
												   passes 			: Vector.<Shader>) : void
		{
			var cubeTexture	: CubeTextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMap'
			);
			var textureSize	: uint					= cubeTexture.size;

			for (var i : uint = 0; i < 6; ++i)
				passes.push(new PCFShadowMapShader(
					lightId,
					lightId + .1 * i,
					new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff),
					i
				));
		}

		private function pushVarianceShadowMappingPass(sceneBindings	: DataBindingsProxy,
													   lightId 			: uint,
													   passes 			: Vector.<Shader>) : void
		{
			var lightType	: uint	= getLightProperty(
				sceneBindings, lightId, 'type'
			);

			if (lightType != PointLight.LIGHT_TYPE)
			{
				var textureResource : ITextureResource	= null;
				var renderTarget	: RenderTarget		= null;

				if (hasShadowBlurPass(sceneBindings, lightId))
					textureResource	= getLightProperty(sceneBindings, lightId, 'rawShadowMap');
				else
					textureResource	= getLightProperty(sceneBindings, lightId, 'shadowMap');

				renderTarget		= new RenderTarget(
					textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
				);

				passes.push(new VarianceShadowMapShader(lightId, 4, lightId + 1, renderTarget));
			}
			else
			{
				var cubeTexture		: CubeTextureResource	= getLightProperty(
					sceneBindings, lightId, 'shadowMap'
				);
				var textureSize		: uint					= cubeTexture.size;

				for (var i : uint = 0; i < 6; ++i)
					passes.push(new VarianceShadowMapShader(
                        lightId,
                        i,
                        lightId + .1 * i,
                        new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff)
					));
			}
		}

		private function pushExponentialShadowMappingPass(sceneBindings	: DataBindingsProxy,
														  lightId 		: uint,
														  passes 		: Vector.<Shader>):void
		{
			var lightType	: uint	= getLightProperty(
				sceneBindings, lightId, 'type'
			);

			if (lightType != PointLight.LIGHT_TYPE)
			{
				var textureResource : ITextureResource	= null;
				var renderTarget	: RenderTarget		= null;

				if (hasShadowBlurPass(sceneBindings, lightId))
					textureResource	= getLightProperty(sceneBindings, lightId, 'rawShadowMap');
				else
					textureResource	= getLightProperty(sceneBindings, lightId, 'shadowMap');

				renderTarget = new RenderTarget(
					textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
				);

				passes.push(new ExponentialShadowMapShader(lightId, 4, lightId + 1, renderTarget));
			}
			else
			{
				var cubeTexture		: CubeTextureResource	= getLightProperty(
					sceneBindings, lightId, 'shadowMap'
				);
				var textureSize		: uint					= cubeTexture.size;

				for (var i : uint = 0; i < 6; ++i)
					passes.push(new ExponentialShadowMapShader(
						lightId,
						i,
						lightId + .1 * i,
						new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff)
					));
			}
		}

		private function hasShadowBlurPass(sceneBindings	: DataBindingsProxy,
							    		   lightId 			: uint) : Boolean
		{
			var quality	: uint	= getLightProperty(sceneBindings, lightId, 'shadowQuality');

			return quality > ShadowMappingQuality.HARD;
		}

		private function lightPropertyExists(sceneBindings 	: DataBindingsProxy,
											 lightId 		: uint,
											 propertyName 	: String) : Boolean
		{
			return sceneBindings.propertyExists(
				LightDataProvider.getLightPropertyName(propertyName, lightId)
			);
		}

		private function getLightProperty(sceneBindings : DataBindingsProxy,
										  lightId 		: uint,
										  propertyName 	: String) : *
		{
			return sceneBindings.getProperty(
				LightDataProvider.getLightPropertyName(propertyName, lightId)
			);
		}
	}
}

package aerys.minko.render.material.phong
{
	import aerys.minko.render.DataBindingsProxy;
	import aerys.minko.render.Effect;
	import aerys.minko.render.RenderTarget;
	import aerys.minko.render.material.phong.multipass.PhongAdditionalShader;
	import aerys.minko.render.material.phong.multipass.PhongAmbientShader;
	import aerys.minko.render.material.phong.multipass.PhongEmissiveShader;
	import aerys.minko.render.material.phong.multipass.ZPrepassShader;
	import aerys.minko.render.material.phong.shadow.ExponentialShadowMapShader;
	import aerys.minko.render.material.phong.shadow.PCFShadowMapShader;
	import aerys.minko.render.material.phong.shadow.ParaboloidShadowMapShader;
	import aerys.minko.render.material.phong.shadow.VarianceShadowMapShader;
	import aerys.minko.render.resource.texture.CubeTextureResource;
	import aerys.minko.render.resource.texture.ITextureResource;
	import aerys.minko.render.resource.texture.TextureResource;
	import aerys.minko.render.shader.Shader;
	import aerys.minko.scene.data.LightDataProvider;
	import aerys.minko.scene.node.light.AmbientLight;
	import aerys.minko.scene.node.light.PointLight;
	import aerys.minko.type.enum.ShadowMappingQuality;
	import aerys.minko.type.enum.ShadowMappingType;

    /**
     *
The PhongEffect using the Phong lighting model to render the geometry according to
     * the lighting setup of the scene. It supports an infinite number of lights/projected
     * shadows and will automatically switch between singlepass and multipass rendering
     * in order to give the best performances whenever possible.

     *
     *
Because of the Stage3D restrictions regarding the number of shader operations or
     * the number of available registers, the number of lights might be to big to allow them
     * to be rendered in a single pass. In this situation, the PhongEffect will automatically
     * fallback and use multipass rendering.

     *
     *
Multipass rendering is done as follow:

     *

     *
The "base" pass renders objects with one per-pixel directional lights with shadows,
     * the lightmap and the ambient/emissive lighting.

     *
Each "additional" pass (one per light) will render a single light with shadows and
     * blend it using additive blending.

     *

     *
     * The singlepass rendering will mimic this behavior in order to get preserve consistency.
     *
     * @author Jean-Marc Le Roux
     *
     */
	public class PhongEffect extends Effect
	{
        private var _useRenderToTexture : Boolean;
        private var _singlePassShader   : Shader;
        private var _emissiveShader     : Shader;

        private var _targets            : Array;

		public function PhongEffect(useRenderToTexture   : Boolean   = false,
                                    singlePassShader     : Shader    = null,
                                    emissiveShader           : Shader    = null)
		{
            super();

            _useRenderToTexture = useRenderToTexture;
            _singlePassShader = singlePassShader || new PhongSinglePassShader(null, 0);
            _emissiveShader = emissiveShader || new PhongEmissiveShader(null, null, .25);

            _targets = [];
		}

        override protected function initializePasses(sceneBindings	: DataBindingsProxy,
                                                     meshBindings	: DataBindingsProxy) : Vector.<Shader>
        {
            var passes : Vector.<Shader>    = super.initializePasses(sceneBindings, meshBindings);

            for (var lightId : uint = 0;
                lightPropertyExists(sceneBindings, lightId, 'enabled')
                    && getLightProperty(sceneBindings, lightId, 'enabled');
                ++lightId)
            {
                if (lightPropertyExists(sceneBindings, lightId, 'shadowCastingType'))
                {
                    var lightType			: uint	= getLightProperty(sceneBindings, lightId, 'type');
                    var shadowMappingType 	: uint	= getLightProperty(
                        sceneBindings, lightId, 'shadowCastingType'
                    );

                    switch (shadowMappingType)
                    {
                        case ShadowMappingType.PCF:
                            if (lightType == PointLight.LIGHT_TYPE)
                                pushCubeShadowMappingPass(sceneBindings, lightId, passes);
                            else
                                pushPCFShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.DUAL_PARABOLOID:
                            pushDualParaboloidShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.VARIANCE:
                            pushVarianceShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                        case ShadowMappingType.EXPONENTIAL:
                            pushExponentialShadowMappingPass(sceneBindings, lightId, passes);
                            break ;
                    }
                }
            }

            passes.push(_singlePassShader);

            return passes;
        }

        override protected function initializeFallbackPasses(sceneBindings  : DataBindingsProxy,
                                                             meshBindings   : DataBindingsProxy) : Vector.<Shader>
        {
            var passes              : Vector.<Shader>   = new <Shader>[];
            var discardDirectional  : Boolean           = true;
            var ambientEnabled      : Boolean           = meshBindings.propertyExists('lightmap');
            var renderTarget        : RenderTarget      = null;

            if (_useRenderToTexture)
            {
                var accumulatorSize : uint  = sceneBindings.getProperty('viewportWidth');

                accumulatorSize = 1 << Math.ceil(Math.log(accumulatorSize) * Math.LOG2E);

                renderTarget = _targets[accumulatorSize] = new RenderTarget(
                    accumulatorSize, accumulatorSize,
                    new TextureResource(accumulatorSize, accumulatorSize)
                );
            }

			for (var lightId : uint = 0;
                lightPropertyExists(sceneBindings, lightId, 'enabled')
                    && getLightProperty(sceneBindings, lightId, 'enabled');
                ++lightId)
			{
                var lightType : uint = getLightProperty(sceneBindings, lightId, 'type');

                if (lightType == AmbientLight.LIGHT_TYPE)
                {
                    ambientEnabled = true;
                    continue;
                }

                if (getLightProperty(sceneBindings, lightId, 'diffuseEnabled'))
                    passes.push(
                        new PhongAdditionalShader(lightId, true, false, renderTarget, .5)
                    );

                if (getLightProperty(sceneBindings, lightId, 'specularEnabled'))
                    passes.push(
                        new PhongAdditionalShader(lightId, false, true, null, 0.)
                    );

				if (lightPropertyExists(sceneBindings, lightId, 'shadowCastingType'))
				{
					var shadowMappingType 	: uint	= getLightProperty(
						sceneBindings, lightId, 'shadowCastingType'
					);

					switch (shadowMappingType)
					{
						case ShadowMappingType.PCF:
							if (lightType == PointLight.LIGHT_TYPE)
								pushCubeShadowMappingPass(sceneBindings, lightId, passes);
							else
								pushPCFShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.DUAL_PARABOLOID:
							pushDualParaboloidShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.VARIANCE:
							pushVarianceShadowMappingPass(sceneBindings, lightId, passes);
							break ;
						case ShadowMappingType.EXPONENTIAL:
							pushExponentialShadowMappingPass(sceneBindings, lightId, passes);
							break ;
					}
				}
			}

//            if (ambientEnabled)
//                passes.push(new PhongAmbientShader(renderTarget, .75));

            passes.push(new ZPrepassShader(renderTarget, 1));
            passes.push(new PhongEmissiveShader(
                renderTarget ? renderTarget.textureResource : null, null, .25
            ));

            return passes;
		}

		private function pushPCFShadowMappingPass(sceneBindings	: DataBindingsProxy,
                                                  lightId 		: uint,
                                                  passes 		: Vector.<Shader>) : void
		{
			var textureResource : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMap'
			);
			var renderTarget	: RenderTarget		= new RenderTarget(
				textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
			);

			passes.push(new PCFShadowMapShader(lightId, lightId + 1, renderTarget));
		}

		private function pushDualParaboloidShadowMappingPass(sceneBindings	: DataBindingsProxy,
															 lightId 		: uint,
															 passes 		: Vector.<Shader>) : void
		{
			var frontTextureResource : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMapFront'
			);
			var backTextureResource	 : TextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMapBack'
			);
			var size				 : uint				= frontTextureResource.width;
			var frontRenderTarget	 : RenderTarget		= new RenderTarget(
				size, size, frontTextureResource, 0, 0xffffffff
			);
			var backRenderTarget	 : RenderTarget		= new RenderTarget(
				size, size, backTextureResource, 0, 0xffffffff
			);

			passes.push(
				new ParaboloidShadowMapShader(lightId, true, lightId + 0.5, frontRenderTarget),
				new ParaboloidShadowMapShader(lightId, false, lightId + 1, backRenderTarget)
			);
		}

		private function pushCubeShadowMappingPass(sceneBindings	: DataBindingsProxy,
												   lightId 			: uint,
												   passes 			: Vector.<Shader>) : void
		{
			var cubeTexture	: CubeTextureResource	= getLightProperty(
				sceneBindings, lightId, 'shadowMap'
			);
			var textureSize	: uint					= cubeTexture.size;

			for (var i : uint = 0; i < 6; ++i)
				passes.push(new PCFShadowMapShader(
					lightId,
					lightId + .1 * i,
					new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff),
					i
				));
		}

		private function pushVarianceShadowMappingPass(sceneBindings	: DataBindingsProxy,
													   lightId 			: uint,
													   passes 			: Vector.<Shader>) : void
		{
			var lightType	: uint	= getLightProperty(
				sceneBindings, lightId, 'type'
			);

			if (lightType != PointLight.LIGHT_TYPE)
			{
				var textureResource : ITextureResource	= null;
				var renderTarget	: RenderTarget		= null;

				if (hasShadowBlurPass(sceneBindings, lightId))
					textureResource	= getLightProperty(sceneBindings, lightId, 'rawShadowMap');
				else
					textureResource	= getLightProperty(sceneBindings, lightId, 'shadowMap');

				renderTarget		= new RenderTarget(
					textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
				);

				passes.push(new VarianceShadowMapShader(lightId, 4, lightId + 1, renderTarget));
			}
			else
			{
				var cubeTexture		: CubeTextureResource	= getLightProperty(
					sceneBindings, lightId, 'shadowMap'
				);
				var textureSize		: uint					= cubeTexture.size;

				for (var i : uint = 0; i < 6; ++i)
					passes.push(new VarianceShadowMapShader(
                        lightId,
                        i,
                        lightId + .1 * i,
                        new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff)
					));
			}
		}

		private function pushExponentialShadowMappingPass(sceneBindings	: DataBindingsProxy,
														  lightId 		: uint,
														  passes 		: Vector.<Shader>):void
		{
			var lightType	: uint	= getLightProperty(
				sceneBindings, lightId, 'type'
			);

			if (lightType != PointLight.LIGHT_TYPE)
			{
				var textureResource : ITextureResource	= null;
				var renderTarget	: RenderTarget		= null;

				if (hasShadowBlurPass(sceneBindings, lightId))
					textureResource	= getLightProperty(sceneBindings, lightId, 'rawShadowMap');
				else
					textureResource	= getLightProperty(sceneBindings, lightId, 'shadowMap');

				renderTarget = new RenderTarget(
					textureResource.width, textureResource.height, textureResource, 0, 0xffffffff
				);

				passes.push(new ExponentialShadowMapShader(lightId, 4, lightId + 1, renderTarget));
			}
			else
			{
				var cubeTexture		: CubeTextureResource	= getLightProperty(
					sceneBindings, lightId, 'shadowMap'
				);
				var textureSize		: uint					= cubeTexture.size;

				for (var i : uint = 0; i < 6; ++i)
					passes.push(new ExponentialShadowMapShader(
						lightId,
						i,
						lightId + .1 * i,
						new RenderTarget(textureSize, textureSize, cubeTexture, i, 0xffffffff)
					));
			}
		}

		private function hasShadowBlurPass(sceneBindings	: DataBindingsProxy,
							    		   lightId 			: uint) : Boolean
		{
			var quality	: uint	= getLightProperty(sceneBindings, lightId, 'shadowQuality');

			return quality > ShadowMappingQuality.HARD;
		}

		private function lightPropertyExists(sceneBindings 	: DataBindingsProxy,
											 lightId 		: uint,
											 propertyName 	: String) : Boolean
		{
			return sceneBindings.propertyExists(
				LightDataProvider.getLightPropertyName(propertyName, lightId)
			);
		}

		private function getLightProperty(sceneBindings : DataBindingsProxy,
										  lightId 		: uint,
										  propertyName 	: String) : *
		{
			return sceneBindings.getProperty(
				LightDataProvider.getLightPropertyName(propertyName, lightId)
			);
		}
	}
}

fix PhongEffect passes init. to avoid pushing passes for lights when light.enabled != true


package com.kaltura.hls.m2ts
{
	import com.kaltura.hls.HLSStreamingResource;
	import com.kaltura.hls.SubtitleTrait;
	import com.kaltura.hls.manifest.HLSManifestEncryptionKey;
	import com.kaltura.hls.muxing.AACParser;
	import com.kaltura.hls.subtitles.SubTitleParser;
	import com.kaltura.hls.subtitles.TextTrackCue;
	import com.kaltura.hls.HLSIndexHandler;

	import flash.utils.ByteArray;
	import flash.utils.IDataInput;
	import flash.utils.getTimer;

	import flash.external.ExternalInterface;

	import org.osmf.events.HTTPStreamingEvent;
	import org.osmf.net.httpstreaming.HTTPStreamingFileHandlerBase;
	import org.osmf.net.httpstreaming.flv.FLVTagAudio;

	/**
	 * Process M2TS data into FLV data and return it for rendering via OSMF video system.
	 */
	public class M2TSFileHandler extends HTTPStreamingFileHandlerBase
	{
		public var subtitleTrait:SubtitleTrait;
		public var key:HLSManifestEncryptionKey;
		public var segmentId:uint = 0;
		public var resource:HLSStreamingResource;
		public var segmentUri:String;
		public var isBestEffort:Boolean = false;

		private var _parser:TSPacketParser;
		private var _curTimeOffset:uint;
		private var _buffer:ByteArray;
		private var _fragReadBuffer:ByteArray;
		private var _encryptedDataBuffer:ByteArray;
		private var _timeOrigin:uint;
		private var _timeOriginNeeded:Boolean;
		private var _segmentBeginSeconds:Number;
		private var _segmentLastSeconds:Number;
		private var _firstSeekTime:Number;
		private var _lastContinuityToken:String;
		private var _extendedIndexHandler:IExtraIndexHandlerState;
		private var _lastFLVMessageTime:Number;
		private var _injectingSubtitles:Boolean = false;
		private var _lastInjectedSubtitleTime:Number = 0;

		private var _decryptionIV:ByteArray;

		public function M2TSFileHandler()
		{
			super();

			_encryptedDataBuffer = new ByteArray();

			_parser = new TSPacketParser();
			_parser.callback = handleFLVMessage;

			_timeOrigin = 0;
			_timeOriginNeeded = true;

			_segmentBeginSeconds = -1;
			_segmentLastSeconds = -1;

			_firstSeekTime = 0;

			_extendedIndexHandler = null;

			_lastContinuityToken = null;
		}

		public function get duration():Number
		{
			if(_segmentLastSeconds > _segmentBeginSeconds)
				return _segmentLastSeconds - _segmentBeginSeconds;
			return -1;
		}

		public function set extendedIndexHandler(handler:IExtraIndexHandlerState):void
		{
			_extendedIndexHandler = handler;
		}

		public function get extendedIndexHandler():IExtraIndexHandlerState
		{
			return _extendedIndexHandler;
		}

		public override function beginProcessFile(seek:Boolean, seekTime:Number):void
		{
			if( key && !key.isLoading && !key.isLoaded)
				throw new Error("Tried to process segment with key not set to load or loaded.");

			if(isBestEffort)
			{
				trace("Doing extra flush for best effort file handler");
				_parser.flush();
				_parser.clear();
			}

			// Decryption reset
			if ( key )
			{
				trace("Resetting _decryptionIV");
				if ( key.iv ) _decryptionIV = key.retrieveStoredIV();
				else _decryptionIV = HLSManifestEncryptionKey.createIVFromID( segmentId );
			}

			var discontinuity:Boolean = false;

			if(_extendedIndexHandler)
			{
				var currentContinuityToken:String = _extendedIndexHandler.getCurrentContinuityToken();

				if(_lastContinuityToken != currentContinuityToken)
					discontinuity = true;
				_lastContinuityToken = currentContinuityToken;
			}

			if(seek)
			{
				_parser.clear();

				_timeOriginNeeded = true;

				if(_extendedIndexHandler)
					_firstSeekTime = _extendedIndexHandler.calculateFileOffsetForTime(seekTime) * 1000.0;
			}
			else if(discontinuity)
			{
				// Kick the converter state, but try to avoid upsetting the audio stream.
				_parser.clear(false);

				if(_segmentLastSeconds >= 0.0)
				{
					_timeOriginNeeded = true;
					if(_extendedIndexHandler)
						_firstSeekTime = _extendedIndexHandler.getCurrentSegmentOffset() * 1000.0;
					else
						_firstSeekTime = _segmentLastSeconds * 1000.0 + 30;
				}
			}
			else if(_extendedIndexHandler && _segmentLastSeconds >= 0.0)
			{
				var currentFileOffset:Number = _extendedIndexHandler.getCurrentSegmentOffset();
				var delta:Number = currentFileOffset - _segmentLastSeconds;

				// If it's a big jump, handle it.
				if(delta > 5.0)
				{
					_timeOriginNeeded = true;
					_firstSeekTime = currentFileOffset * 1000.0;
				}
			}

			_segmentBeginSeconds = -1;
			_segmentLastSeconds = -1;
			_lastInjectedSubtitleTime = -1;
			_encryptedDataBuffer.length = 0;

			// Note the start as a debug event.
			_parser.sendDebugEvent( {type:"segmentStart", uri:segmentUri});
		}

		public override function get inputBytesNeeded():Number
		{
			return 0;
		}

		public static var tmpBuffer:ByteArray = new ByteArray();

		private function basicProcessFileSegment(input:IDataInput, _flush:Boolean):ByteArray
		{
			if ( key && !key.isLoaded )
			{
				trace("basicProcessFileSegment - Waiting on key to download.");
				if(input)
					input.readBytes( _encryptedDataBuffer, _encryptedDataBuffer.length );
				return null;
			}

			tmpBuffer.position = 0;
			tmpBuffer.length = 0;

			if ( _encryptedDataBuffer.length > 0 )
			{
				// Restore any pending encrypted data.
				trace("Restoring " + _encryptedDataBuffer.length + " bytes of encrypted data.");
				_encryptedDataBuffer.position = 0;
				_encryptedDataBuffer.readBytes( tmpBuffer );
				_encryptedDataBuffer.clear();
			}

			if(!input)
				input = new ByteArray();

			var amountToRead:int = input.bytesAvailable;
			if(amountToRead > 1024*128) amountToRead = 1024*128;
			trace("READING " + amountToRead + " OF " + input.bytesAvailable);
			if(amountToRead > 0)
				input.readBytes( tmpBuffer, tmpBuffer.length, amountToRead);

			if ( key )
			{
				// We need to decrypt available data.
				var bytesToRead:uint = tmpBuffer.length;
				var leftoverBytes:uint = bytesToRead % 16;
				bytesToRead -= leftoverBytes;

				trace("Decrypting " + tmpBuffer.length + " bytes of encrypted data.");

				key.usePadding = false;

				if ( leftoverBytes > 0 )
				{
					// Place any bytes left over (not divisible by 16) into our encrypted buffer
					// to decrypt later, when we have more bytes
					tmpBuffer.position = bytesToRead;
					tmpBuffer.readBytes( _encryptedDataBuffer );
					tmpBuffer.length = bytesToRead;
					trace("Storing " + _encryptedDataBuffer.length + " bytes of encrypted data.");
				}
				else
				{
					// Attempt to unpad if our buffer is equally divisible by 16.
					// It could mean that we've reached the end of the file segment.
					key.usePadding = true;
				}

				// Store our current IV so we can use it do decrypt
				var currentIV:ByteArray = _decryptionIV;

				// Stash the IV for our next set of bytes - last block of the ciphertext.
				_decryptionIV = new ByteArray();
				tmpBuffer.position = bytesToRead - 16;
				tmpBuffer.readBytes( _decryptionIV );

				// Aaaaand... decrypt!
				tmpBuffer = key.decrypt( tmpBuffer, currentIV );
			}

			// If it's AAC, process it.
			if(AACParser.probe(tmpBuffer))
			{
				//trace("GOT AAC " + tmpBuffer.bytesAvailable);
				var aac:AACParser = new AACParser();
				aac.parse(tmpBuffer, _fragReadHandler);
				//trace("    - returned " + _fragReadBuffer.length + " bytes!");
				_fragReadBuffer.position = 0;

				if(isBestEffort && _fragReadBuffer.length > 0)
				{
					trace("Discarding AAC data from best effort.");
					_fragReadBuffer.length = 0;
				}

				return _fragReadBuffer;
			}

			// Parse it as MPEG TS data.
			var buffer:ByteArray = new ByteArray();
			_buffer = buffer;
			_parser.appendBytes(tmpBuffer);
			if ( _flush )
			{
				trace("flushing parser");
				_parser.flush();
			}
			_buffer = null;
			buffer.position = 0;

			// Throw it out if it's a best effort fetch.
			if(isBestEffort && buffer.length > 0)
			{
				trace("Discarding normal data from best effort.");
				buffer.length = 0;
			}

			if(buffer.length == 0)
				return null;

			return buffer;
		}

		private function _fragReadHandler(audioTags:Vector.<FLVTagAudio>, adif:ByteArray):void
		{
			_fragReadBuffer = new ByteArray();
			var audioTag:FLVTagAudio = new FLVTagAudio();
			audioTag.soundFormat = FLVTagAudio.SOUND_FORMAT_AAC;
			audioTag.data = adif;
			audioTag.isAACSequenceHeader = true;
			audioTag.write(_fragReadBuffer);

			for(var i:int=0; i<audioTags.length; i++)
				audioTags[i].write(_fragReadBuffer);
		}

		public override function processFileSegment(input:IDataInput):ByteArray
		{
			return basicProcessFileSegment(input, false);
		}

		public override function endProcessFile(input:IDataInput):ByteArray
		{
			if ( key && !key.isLoaded ) trace("HIT END OF FILE WITH NO KEY!");

			if ( key ) key.usePadding = true;

			// Note the start as a debug event.
			_parser.sendDebugEvent( {type:"segmentEnd", uri:segmentUri});

			var rv:ByteArray = basicProcessFileSegment(input, true);

			var elapsed:Number = _segmentLastSeconds - _segmentBeginSeconds;

			// Also update end time - don't trace it as we'll increase it incrementally.
			if(HLSIndexHandler.endTimeWitnesses[segmentUri] == null)
			{
				trace("Noting segment end time for " + segmentUri + " of " + _segmentLastSeconds);
				if(_segmentLastSeconds != _segmentLastSeconds)
					throw new Error("Got a NaN _segmentLastSeconds for " + segmentUri + "!");
				HLSIndexHandler.endTimeWitnesses[segmentUri] = _segmentLastSeconds;
			}

			if(elapsed <= 0.0 && _extendedIndexHandler)
			{
				elapsed = _extendedIndexHandler.getTargetSegmentDuration(); // XXX fudge hack!
			}

			dispatchEvent(new HTTPStreamingEvent(HTTPStreamingEvent.FRAGMENT_DURATION, false, false, elapsed));

			return rv;
		}

		public override function flushFileSegment(input:IDataInput):ByteArray
		{
			return basicProcessFileSegment(input || new ByteArray(), true);
		}

		private function handleFLVMessage(timestamp:uint, message:ByteArray):void
		{
			var timestampSeconds:Number = timestamp / 1000.0;

			if(_segmentBeginSeconds < 0)
			{
				_segmentBeginSeconds = timestampSeconds;
				trace("Noting segment start time for " + segmentUri + " of " + timestampSeconds);
				HLSIndexHandler.startTimeWitnesses[segmentUri] = timestampSeconds;
			}

			if(timestampSeconds > _segmentLastSeconds)
				_segmentLastSeconds = timestampSeconds;

			if(isBestEffort)
				return;

			var type:int = message[0];
			ExternalInterface.call("onTag(" + timestampSeconds + ", " + type + ")");

			//trace("Got " + message.length + " bytes at " + timestampSeconds + " seconds");

			if(_timeOriginNeeded)
			{
				_timeOrigin = timestamp;
				_timeOriginNeeded = false;
			}

			if(timestamp < _timeOrigin)
				_timeOrigin = timestamp;

			// Encode the timestamp.
			message[6] = (timestamp      ) & 0xff;
			message[5] = (timestamp >>  8) & 0xff;
			message[4] = (timestamp >> 16) & 0xff;
			message[7] = (timestamp >> 24) & 0xff;

			var lastMsgTime:Number = _lastFLVMessageTime;
			_lastFLVMessageTime = timestampSeconds;

			// If timer was reset due to seek, reset last subtitle time
			if(timestampSeconds < _lastInjectedSubtitleTime)
			{
				trace("Bumping back on subtitle threshold.")
				_lastInjectedSubtitleTime = timestampSeconds;
			}

			// Inject any subtitle tags between messages
			injectSubtitles( _lastInjectedSubtitleTime + 0.001, timestampSeconds );

			//trace( "MESSAGE RECEIVED " + timestampSeconds );

			_buffer.writeBytes(message);
		}

		protected var _lastCue:TextTrackCue = null;

		private function injectSubtitles( startTime:Number, endTime:Number ):void
		{
			//if(startTime > endTime) trace("***** BAD BEHAVIOR " + startTime + " " + endTime);

			//trace("Inject subtitles " + startTime + " " + endTime);

			// Early out if no subtitles, no time has elapsed or we are already injecting subtitles
			if ( !subtitleTrait || endTime - startTime <= 0 || _injectingSubtitles ) return;

			var subtitles:Vector.<SubTitleParser> = subtitleTrait.activeSubtitles;
			if ( !subtitles ) return;

			_injectingSubtitles = true;

			var subtitleCount:int = subtitles.length;
			for ( var i:int = 0; i < subtitleCount; i++ )
			{
				var subtitle:SubTitleParser = subtitles[ i ];
				if ( subtitle.startTime > endTime ) break;
				var cues:Vector.<TextTrackCue> = subtitle.textTrackCues;
				var cueCount:int = cues.length;

				var potentials:Vector.<TextTrackCue> = new Vector.<TextTrackCue>();

				for ( var j:int = 0; j < cueCount; j++ )
				{
					var cue:TextTrackCue = cues[ j ];
					if ( cue.startTime > endTime ) break;
					else if ( cue.startTime >= startTime )
					{
						potentials.push(cue);
					}
				}

				if(potentials.length > 0)
				{
					// TODO: Add support for trackid
					cue = potentials[potentials.length - 1];
					if(cue != _lastCue)
					{
						_parser.createAndSendCaptionMessage( cue.startTime, cue.text, subtitleTrait.language );
						_lastInjectedSubtitleTime = cue.startTime;
						_lastCue = cue;
					}
				}
			}

			_injectingSubtitles = false;
		}
	}
}

package com.kaltura.hls.m2ts
{
	import com.kaltura.hls.HLSStreamingResource;
	import com.kaltura.hls.SubtitleTrait;
	import com.kaltura.hls.manifest.HLSManifestEncryptionKey;
	import com.kaltura.hls.muxing.AACParser;
	import com.kaltura.hls.subtitles.SubTitleParser;
	import com.kaltura.hls.subtitles.TextTrackCue;
	import com.kaltura.hls.HLSIndexHandler;

	import flash.utils.ByteArray;
	import flash.utils.IDataInput;
	import flash.utils.getTimer;

	import flash.external.ExternalInterface;

	import org.osmf.events.HTTPStreamingEvent;
	import org.osmf.net.httpstreaming.HTTPStreamingFileHandlerBase;
	import org.osmf.net.httpstreaming.flv.FLVTagAudio;

	/**
	 * Process M2TS data into FLV data and return it for rendering via OSMF video system.
	 */
	public class M2TSFileHandler extends HTTPStreamingFileHandlerBase
	{
		public var subtitleTrait:SubtitleTrait;
		public var key:HLSManifestEncryptionKey;
		public var segmentId:uint = 0;
		public var resource:HLSStreamingResource;
		public var segmentUri:String;
		public var isBestEffort:Boolean = false;

		private var _parser:TSPacketParser;
		private var _curTimeOffset:uint;
		private var _buffer:ByteArray;
		private var _fragReadBuffer:ByteArray;
		private var _encryptedDataBuffer:ByteArray;
		private var _timeOrigin:uint;
		private var _timeOriginNeeded:Boolean;
		private var _segmentBeginSeconds:Number;
		private var _segmentLastSeconds:Number;
		private var _firstSeekTime:Number;
		private var _lastContinuityToken:String;
		private var _extendedIndexHandler:IExtraIndexHandlerState;
		private var _lastFLVMessageTime:Number;
		private var _injectingSubtitles:Boolean = false;
		private var _lastInjectedSubtitleTime:Number = 0;

		private var _decryptionIV:ByteArray;

		public function M2TSFileHandler()
		{
			super();

			_encryptedDataBuffer = new ByteArray();

			_parser = new TSPacketParser();
			_parser.callback = handleFLVMessage;

			_timeOrigin = 0;
			_timeOriginNeeded = true;

			_segmentBeginSeconds = -1;
			_segmentLastSeconds = -1;

			_firstSeekTime = 0;

			_extendedIndexHandler = null;

			_lastContinuityToken = null;
		}

		public function get duration():Number
		{
			if(_segmentLastSeconds > _segmentBeginSeconds)
				return _segmentLastSeconds - _segmentBeginSeconds;
			return -1;
		}

		public function set extendedIndexHandler(handler:IExtraIndexHandlerState):void
		{
			_extendedIndexHandler = handler;
		}

		public function get extendedIndexHandler():IExtraIndexHandlerState
		{
			return _extendedIndexHandler;
		}

		public override function beginProcessFile(seek:Boolean, seekTime:Number):void
		{
			if( key && !key.isLoading && !key.isLoaded)
				throw new Error("Tried to process segment with key not set to load or loaded.");

			if(isBestEffort)
			{
				trace("Doing extra flush for best effort file handler");
				_parser.flush();
				_parser.clear();
			}

			// Decryption reset
			if ( key )
			{
				trace("Resetting _decryptionIV");
				if ( key.iv ) _decryptionIV = key.retrieveStoredIV();
				else _decryptionIV = HLSManifestEncryptionKey.createIVFromID( segmentId );
			}

			var discontinuity:Boolean = false;

			if(_extendedIndexHandler)
			{
				var currentContinuityToken:String = _extendedIndexHandler.getCurrentContinuityToken();

				if(_lastContinuityToken != currentContinuityToken)
					discontinuity = true;
				_lastContinuityToken = currentContinuityToken;
			}

			if(seek)
			{
				_parser.clear();

				_timeOriginNeeded = true;

				if(_extendedIndexHandler)
					_firstSeekTime = _extendedIndexHandler.calculateFileOffsetForTime(seekTime) * 1000.0;
			}
			else if(discontinuity)
			{
				// Kick the converter state, but try to avoid upsetting the audio stream.
				_parser.clear(false);

				if(_segmentLastSeconds >= 0.0)
				{
					_timeOriginNeeded = true;
					if(_extendedIndexHandler)
						_firstSeekTime = _extendedIndexHandler.getCurrentSegmentOffset() * 1000.0;
					else
						_firstSeekTime = _segmentLastSeconds * 1000.0 + 30;
				}
			}
			else if(_extendedIndexHandler && _segmentLastSeconds >= 0.0)
			{
				var currentFileOffset:Number = _extendedIndexHandler.getCurrentSegmentOffset();
				var delta:Number = currentFileOffset - _segmentLastSeconds;

				// If it's a big jump, handle it.
				if(delta > 5.0)
				{
					_timeOriginNeeded = true;
					_firstSeekTime = currentFileOffset * 1000.0;
				}
			}

			_segmentBeginSeconds = -1;
			_segmentLastSeconds = -1;
			_lastInjectedSubtitleTime = -1;
			_encryptedDataBuffer.length = 0;

			// Note the start as a debug event.
			_parser.sendDebugEvent( {type:"segmentStart", uri:segmentUri});
		}

		public override function get inputBytesNeeded():Number
		{
			return 0;
		}

		public static var tmpBuffer:ByteArray = new ByteArray();

		private function basicProcessFileSegment(input:IDataInput, _flush:Boolean):ByteArray
		{
			if ( key && !key.isLoaded )
			{
				trace("basicProcessFileSegment - Waiting on key to download.");
				if(input)
					input.readBytes( _encryptedDataBuffer, _encryptedDataBuffer.length );
				return null;
			}

			tmpBuffer.position = 0;
			tmpBuffer.length = 0;

			if ( _encryptedDataBuffer.length > 0 )
			{
				// Restore any pending encrypted data.
				trace("Restoring " + _encryptedDataBuffer.length + " bytes of encrypted data.");
				_encryptedDataBuffer.position = 0;
				_encryptedDataBuffer.readBytes( tmpBuffer );
				_encryptedDataBuffer.clear();
			}

			if(!input)
				input = new ByteArray();

			var amountToRead:int = input.bytesAvailable;
			if(amountToRead > 1024*128) amountToRead = 1024*128;
			trace("READING " + amountToRead + " OF " + input.bytesAvailable);
			if(amountToRead > 0)
				input.readBytes( tmpBuffer, tmpBuffer.length, amountToRead);

			if ( key )
			{
				// We need to decrypt available data.
				var bytesToRead:uint = tmpBuffer.length;
				var leftoverBytes:uint = bytesToRead % 16;
				bytesToRead -= leftoverBytes;

				trace("Decrypting " + tmpBuffer.length + " bytes of encrypted data.");

				key.usePadding = false;

				if ( leftoverBytes > 0 )
				{
					// Place any bytes left over (not divisible by 16) into our encrypted buffer
					// to decrypt later, when we have more bytes
					tmpBuffer.position = bytesToRead;
					tmpBuffer.readBytes( _encryptedDataBuffer );
					tmpBuffer.length = bytesToRead;
					trace("Storing " + _encryptedDataBuffer.length + " bytes of encrypted data.");
				}
				else
				{
					// Attempt to unpad if our buffer is equally divisible by 16.
					// It could mean that we've reached the end of the file segment.
					key.usePadding = true;
				}

				// Store our current IV so we can use it do decrypt
				var currentIV:ByteArray = _decryptionIV;

				// Stash the IV for our next set of bytes - last block of the ciphertext.
				_decryptionIV = new ByteArray();
				tmpBuffer.position = bytesToRead - 16;
				tmpBuffer.readBytes( _decryptionIV );

				// Aaaaand... decrypt!
				tmpBuffer = key.decrypt( tmpBuffer, currentIV );
			}

			// If it's AAC, process it.
			if(AACParser.probe(tmpBuffer))
			{
				//trace("GOT AAC " + tmpBuffer.bytesAvailable);
				var aac:AACParser = new AACParser();
				aac.parse(tmpBuffer, _fragReadHandler);
				//trace("    - returned " + _fragReadBuffer.length + " bytes!");
				_fragReadBuffer.position = 0;

				if(isBestEffort && _fragReadBuffer.length > 0)
				{
					trace("Discarding AAC data from best effort.");
					_fragReadBuffer.length = 0;
				}

				return _fragReadBuffer;
			}

			// Parse it as MPEG TS data.
			var buffer:ByteArray = new ByteArray();
			_buffer = buffer;
			_parser.appendBytes(tmpBuffer);
			if ( _flush )
			{
				trace("flushing parser");
				_parser.flush();
			}
			_buffer = null;
			buffer.position = 0;

			// Throw it out if it's a best effort fetch.
			if(isBestEffort && buffer.length > 0)
			{
				trace("Discarding normal data from best effort.");
				buffer.length = 0;
			}

			if(buffer.length == 0)
				return null;

			return buffer;
		}

		private function _fragReadHandler(audioTags:Vector.<FLVTagAudio>, adif:ByteArray):void
		{
			_fragReadBuffer = new ByteArray();
			var audioTag:FLVTagAudio = new FLVTagAudio();
			audioTag.soundFormat = FLVTagAudio.SOUND_FORMAT_AAC;
			audioTag.data = adif;
			audioTag.isAACSequenceHeader = true;
			audioTag.write(_fragReadBuffer);

			for(var i:int=0; i<audioTags.length; i++)
				audioTags[i].write(_fragReadBuffer);
		}

		public override function processFileSegment(input:IDataInput):ByteArray
		{
			return basicProcessFileSegment(input, false);
		}

		public override function endProcessFile(input:IDataInput):ByteArray
		{
			if ( key && !key.isLoaded ) trace("HIT END OF FILE WITH NO KEY!");

			if ( key ) key.usePadding = true;

			// Note the start as a debug event.
			_parser.sendDebugEvent( {type:"segmentEnd", uri:segmentUri});

			var rv:ByteArray = basicProcessFileSegment(input, true);

			var elapsed:Number = _segmentLastSeconds - _segmentBeginSeconds;

			// Also update end time - don't trace it as we'll increase it incrementally.
			if(HLSIndexHandler.endTimeWitnesses[segmentUri] == null && !isBestEffort)
			{
				trace("Noting segment end time for " + segmentUri + " of " + _segmentLastSeconds);
				if(_segmentLastSeconds != _segmentLastSeconds)
					throw new Error("Got a NaN _segmentLastSeconds for " + segmentUri + "!");
				HLSIndexHandler.endTimeWitnesses[segmentUri] = _segmentLastSeconds;
			}

			if(elapsed <= 0.0 && _extendedIndexHandler)
			{
				elapsed = _extendedIndexHandler.getTargetSegmentDuration(); // XXX fudge hack!
			}

			dispatchEvent(new HTTPStreamingEvent(HTTPStreamingEvent.FRAGMENT_DURATION, false, false, elapsed));

			return rv;
		}

		public override function flushFileSegment(input:IDataInput):ByteArray
		{
			return basicProcessFileSegment(input || new ByteArray(), true);
		}

		private function handleFLVMessage(timestamp:uint, message:ByteArray):void
		{
			var timestampSeconds:Number = timestamp / 1000.0;

			if(_segmentBeginSeconds < 0)
			{
				_segmentBeginSeconds = timestampSeconds;
				trace("Noting segment start time for " + segmentUri + " of " + timestampSeconds);
				HLSIndexHandler.startTimeWitnesses[segmentUri] = timestampSeconds;
			}

			if(timestampSeconds > _segmentLastSeconds)
				_segmentLastSeconds = timestampSeconds;

			if(isBestEffort)
				return;

			var type:int = message[0];
			ExternalInterface.call("onTag(" + timestampSeconds + ", " + type + ")");

			//trace("Got " + message.length + " bytes at " + timestampSeconds + " seconds");

			if(_timeOriginNeeded)
			{
				_timeOrigin = timestamp;
				_timeOriginNeeded = false;
			}

			if(timestamp < _timeOrigin)
				_timeOrigin = timestamp;

			// Encode the timestamp.
			message[6] = (timestamp      ) & 0xff;
			message[5] = (timestamp >>  8) & 0xff;
			message[4] = (timestamp >> 16) & 0xff;
			message[7] = (timestamp >> 24) & 0xff;

			var lastMsgTime:Number = _lastFLVMessageTime;
			_lastFLVMessageTime = timestampSeconds;

			// If timer was reset due to seek, reset last subtitle time
			if(timestampSeconds < _lastInjectedSubtitleTime)
			{
				trace("Bumping back on subtitle threshold.")
				_lastInjectedSubtitleTime = timestampSeconds;
			}

			// Inject any subtitle tags between messages
			injectSubtitles( _lastInjectedSubtitleTime + 0.001, timestampSeconds );

			//trace( "MESSAGE RECEIVED " + timestampSeconds );

			_buffer.writeBytes(message);
		}

		protected var _lastCue:TextTrackCue = null;

		private function injectSubtitles( startTime:Number, endTime:Number ):void
		{
			//if(startTime > endTime) trace("***** BAD BEHAVIOR " + startTime + " " + endTime);

			//trace("Inject subtitles " + startTime + " " + endTime);

			// Early out if no subtitles, no time has elapsed or we are already injecting subtitles
			if ( !subtitleTrait || endTime - startTime <= 0 || _injectingSubtitles ) return;

			var subtitles:Vector.<SubTitleParser> = subtitleTrait.activeSubtitles;
			if ( !subtitles ) return;

			_injectingSubtitles = true;

			var subtitleCount:int = subtitles.length;
			for ( var i:int = 0; i < subtitleCount; i++ )
			{
				var subtitle:SubTitleParser = subtitles[ i ];
				if ( subtitle.startTime > endTime ) break;
				var cues:Vector.<TextTrackCue> = subtitle.textTrackCues;
				var cueCount:int = cues.length;

				var potentials:Vector.<TextTrackCue> = new Vector.<TextTrackCue>();

				for ( var j:int = 0; j < cueCount; j++ )
				{
					var cue:TextTrackCue = cues[ j ];
					if ( cue.startTime > endTime ) break;
					else if ( cue.startTime >= startTime )
					{
						potentials.push(cue);
					}
				}

				if(potentials.length > 0)
				{
					// TODO: Add support for trackid
					cue = potentials[potentials.length - 1];
					if(cue != _lastCue)
					{
						_parser.createAndSendCaptionMessage( cue.startTime, cue.text, subtitleTrait.language );
						_lastInjectedSubtitleTime = cue.startTime;
						_lastCue = cue;
					}
				}
			}

			_injectingSubtitles = false;
		}
	}
}

Fix bad segment end estimation.

Fix bad segment end estimation.


/* 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.stream {
    import org.mangui.hls.controller.BufferThresholdController;
    import org.mangui.hls.event.HLSPlayMetrics;
    import org.mangui.hls.event.HLSError;
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.constant.HLSSeekStates;
    import org.mangui.hls.constant.HLSPlayStates;
    import org.mangui.hls.flv.FLVTag;
    import org.mangui.hls.HLS;
    import org.mangui.hls.HLSSettings;
    import org.mangui.hls.utils.Hex;

    import flash.events.Event;
    import flash.events.NetStatusEvent;
    import flash.events.TimerEvent;
    import flash.net.*;
    import flash.utils.*;

    CONFIG::LOGGING {
        import org.mangui.hls.utils.Log;
    }
    /** Class that overrides standard flash.net.NetStream class, keeps the buffer filled, handles seek and play state
     *
     * play state transition :
     * 				FROM								TO								condition
     *  HLSPlayStates.IDLE              	HLSPlayStates.PLAYING_BUFFERING     play()/play2()/seek() called
     *  HLSPlayStates.PLAYING_BUFFERING  	HLSPlayStates.PLAYING  				buflen > minBufferLength
     *  HLSPlayStates.PAUSED_BUFFERING  	HLSPlayStates.PAUSED  				buflen > minBufferLength
     *  HLSPlayStates.PLAYING  				HLSPlayStates.PLAYING_BUFFERING  	buflen < lowBufferLength
     *  HLSPlayStates.PAUSED  				HLSPlayStates.PAUSED_BUFFERING  	buflen < lowBufferLength
     */
    public class HLSNetStream extends NetStream {
        /** Reference to the framework controller. **/
        private var _hls : HLS;
        /** reference to buffer threshold controller */
        private var _bufferThresholdController : BufferThresholdController;
        /** FLV Tag Buffer . **/
        private var _streamBuffer : StreamBuffer;
        /** Timer used to check buffer and position. **/
        private var _timer : Timer;
        /** Current playback state. **/
        private var _playbackState : String;
        /** Current seek state. **/
        private var _seekState : String;
        /** current playback level **/
        private var _playbackLevel : int;
        /** Netstream client proxy */
        private var _client : HLSNetStreamClient;

        /** Create the buffer. **/
        public function HLSNetStream(connection : NetConnection, hls : HLS, streamBuffer : StreamBuffer) : void {
            super(connection);
            super.bufferTime = 0.1;
            _hls = hls;
            _bufferThresholdController = new BufferThresholdController(hls);
            _streamBuffer = streamBuffer;
            _playbackState = HLSPlayStates.IDLE;
            _seekState = HLSSeekStates.IDLE;
            _timer = new Timer(100, 0);
            _timer.addEventListener(TimerEvent.TIMER, _checkBuffer);
            _client = new HLSNetStreamClient();
            _client.registerCallback("onHLSFragmentChange", onHLSFragmentChange);
            _client.registerCallback("onID3Data", onID3Data);
            super.client = _client;
        };

        public function onHLSFragmentChange(level : int, seqnum : int, cc : int, audio_only : Boolean, program_date : Number, width : int, height : int, ... tags) : void {
            CONFIG::LOGGING {
                Log.debug("playing fragment(level/sn/cc):" + level + "/" + seqnum + "/" + cc);
            }
            _playbackLevel = level;
            var tag_list : Array = new Array();
            for (var i : uint = 0; i < tags.length; i++) {
                tag_list.push(tags[i]);
                CONFIG::LOGGING {
                    Log.debug("custom tag:" + tags[i]);
                }
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_PLAYING, new HLSPlayMetrics(level, seqnum, cc, audio_only, program_date, width, height, tag_list)));
        }

        // function is called by SCRIPT in FLV
        public function onID3Data(data : ByteArray) : void {
            var dump : String = "unset";

            // we dump the content as hex to get it to the Javascript in the browser.
            // from lots of searching, we could use base64, but even then, the decode would
            // not be native, so hex actually seems more efficient
            dump = Hex.fromArray(data);

            CONFIG::LOGGING {
                Log.debug("id3:" + dump);
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ID3_UPDATED, dump));
        }

        /** timer function, check/update NetStream state, and append tags if needed **/
        private function _checkBuffer(e : Event) : void {
            var buffer : Number = this.bufferLength;
            // Log.info("netstream/total:" + super.bufferLength + "/" + this.bufferLength);
            // Set playback state. no need to check buffer status if seeking
            if (_seekState != HLSSeekStates.SEEKING) {
                // check low buffer condition
                if (buffer <= 0.1) {
                    if (_streamBuffer.reachedEnd) {
                        // Last tag done? Then append sequence end.
                        super.appendBytesAction(NetStreamAppendBytesAction.END_SEQUENCE);
                        super.appendBytes(new ByteArray());
                        // reach end of playlist + playback complete (as buffer is empty).
                        // stop timer, report event and switch to IDLE mode.
                        _timer.stop();
                        CONFIG::LOGGING {
                            Log.debug("reached end of VOD playlist, notify playback complete");
                        }
                        _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_COMPLETE));
                        _setPlaybackState(HLSPlayStates.IDLE);
                        _setSeekState(HLSSeekStates.IDLE);
                        return;
                    } else {
                        // buffer <= 0.1 and not EOS, pause playback
                        super.pause();
                    }
                }
                // if buffer len is below lowBufferLength, get into buffering state
                if (!_streamBuffer.reachedEnd && buffer < _bufferThresholdController.lowBufferLength) {
                    if (_playbackState == HLSPlayStates.PLAYING) {
                        // low buffer condition and play state. switch to play buffering state
                        _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
                    } else if (_playbackState == HLSPlayStates.PAUSED) {
                        // low buffer condition and pause state. switch to paused buffering state
                        _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
                    }
                }
                // if buffer len is above minBufferLength, get out of buffering state
                if (buffer >= _bufferThresholdController.minBufferLength || _streamBuffer.reachedEnd) {
                    if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
                        CONFIG::LOGGING {
                            Log.debug("resume playback");
                        }
                        // resume playback in case it was paused, this can happen if buffer was in really low condition (less than 0.1s)
                        super.resume();
                        _setPlaybackState(HLSPlayStates.PLAYING);
                    } else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
                        _setPlaybackState(HLSPlayStates.PAUSED);
                    }
                }
            }
        };

        /** Return the current playback state. **/
        public function get playbackState() : String {
            return _playbackState;
        };

        /** Return the current seek state. **/
        public function get seekState() : String {
            return _seekState;
        };

        /** Return the current playback quality level **/
        public function get playbackLevel() : int {
            return _playbackLevel;
        };

        /** append tags to NetStream **/
        public function appendTags(tags : Vector.<FLVTag>) : void {
            if (_seekState == HLSSeekStates.SEEKING) {
                /* this is our first injection after seek(),
                let's flush netstream now
                this is to avoid black screen during seek command */
                super.close();
                CONFIG::FLASH_11_1 {
                    try {
                        super.useHardwareDecoder = HLSSettings.useHardwareVideoDecoder;
                    } catch(e : Error) {
                    }
                }
                super.play(null);
                super.appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK);
                // immediatly pause NetStream, it will be resumed when enough data will be buffered in the NetStream
                super.pause();
            }
            // append all tags
            for each (var tagBuffer : FLVTag in tags) {
                try {
                    if (tagBuffer.type == FLVTag.DISCONTINUITY) {
                        super.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
                        super.appendBytes(FLVTag.getHeader());
                    }
                    super.appendBytes(tagBuffer.data);
                } catch (error : Error) {
                    var hlsError : HLSError = new HLSError(HLSError.TAG_APPENDING_ERROR, null, tagBuffer.type + ": " + error.message);
                    _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
                }
            }
            if (_seekState == HLSSeekStates.SEEKING) {
                // dispatch event to mimic NetStream behaviour
                dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, {code:"NetStream.Seek.Notify", level:"status"}));
                _setSeekState(HLSSeekStates.SEEKED);
            }
        };

        /** Change playback state. **/
        private function _setPlaybackState(state : String) : void {
            if (state != _playbackState) {
                CONFIG::LOGGING {
                    Log.debug('[PLAYBACK_STATE] from ' + _playbackState + ' to ' + state);
                }
                _playbackState = state;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_STATE, _playbackState));
            }
        };

        /** Change seeking state. **/
        private function _setSeekState(state : String) : void {
            if (state != _seekState) {
                CONFIG::LOGGING {
                    Log.debug('[SEEK_STATE] from ' + _seekState + ' to ' + state);
                }
                _seekState = state;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.SEEK_STATE, _seekState));
            }
        };

        override public function play(...args) : void {
            var _playStart : Number;
            if (args.length >= 2) {
                _playStart = Number(args[1]);
            } else {
                _playStart = -1;
            }
            CONFIG::LOGGING {
                Log.info("HLSNetStream:play(" + _playStart + ")");
            }
            seek(_playStart);
            _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
        }

        override public function play2(param : NetStreamPlayOptions) : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:play2(" + param.start + ")");
            }
            seek(param.start);
            _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
        }

        /** Pause playback. **/
        override public function pause() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:pause");
            }
            if (_playbackState == HLSPlayStates.PLAYING) {
                super.pause();
                _setPlaybackState(HLSPlayStates.PAUSED);
            } else if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
                super.pause();
                _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
            }
        };

        /** Resume playback. **/
        override public function resume() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:resume");
            }
            if (_playbackState == HLSPlayStates.PAUSED) {
                super.resume();
                _setPlaybackState(HLSPlayStates.PLAYING);
            } else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
                // dont resume NetStream here, it will be resumed by Timer. this avoids resuming playback while seeking is in progress
                _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
            }
        };

        /** get Buffer Length  **/
        override public function get bufferLength() : Number {
            return netStreamBufferLength + _streamBuffer.bufferLength;
        };

        /** get Back Buffer Length  **/
        override public function get backBufferLength() : Number {
            return _streamBuffer.backBufferLength;
        };

        public function get netStreamBufferLength() : Number {
            if (_seekState == HLSSeekStates.SEEKING) {
                return 0;
            } else {
                return super.bufferLength;
            }
        };

        /** Start playing data in the buffer. **/
        override public function seek(position : Number) : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:seek(" + position + ")");
            }
            _streamBuffer.seek(position);
            _setSeekState(HLSSeekStates.SEEKING);
            /* if HLS was in paused state before seeking,
             * switch to paused buffering state
             * otherwise, switch to playing buffering state
             */
            switch(_playbackState) {
                case HLSPlayStates.PAUSED:
                case HLSPlayStates.PAUSED_BUFFERING:
                    _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
                    break;
                case HLSPlayStates.PLAYING:
                case HLSPlayStates.PLAYING_BUFFERING:
                    _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
                    break;
                default:
                    break;
            }
            /* always pause NetStream while seeking, even if we are in play state
             * in that case, NetStream will be resumed during next call to appendTags()
             */
            super.pause();
            _timer.start();
        };

        public override function set client(client : Object) : void {
            _client.delegate = client;
        };

        public override function get client() : Object {
            return _client.delegate;
        }

        /** Stop playback. **/
        override public function close() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:close");
            }
            super.close();
            _streamBuffer.stop();
            _timer.stop();
            _setPlaybackState(HLSPlayStates.IDLE);
            _setSeekState(HLSSeekStates.IDLE);
        };

        public function dispose_() : void {
            close();
            _bufferThresholdController.dispose();
        }
    }
}

/* 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.stream {
    import org.mangui.hls.controller.BufferThresholdController;
    import org.mangui.hls.event.HLSPlayMetrics;
    import org.mangui.hls.event.HLSError;
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.constant.HLSSeekStates;
    import org.mangui.hls.constant.HLSPlayStates;
    import org.mangui.hls.flv.FLVTag;
    import org.mangui.hls.HLS;
    import org.mangui.hls.HLSSettings;
    import org.mangui.hls.utils.Hex;

    import flash.events.Event;
    import flash.events.NetStatusEvent;
    import flash.events.TimerEvent;
    import flash.net.*;
    import flash.utils.*;

    CONFIG::LOGGING {
        import org.mangui.hls.utils.Log;
    }
    /** Class that overrides standard flash.net.NetStream class, keeps the buffer filled, handles seek and play state
     *
     * play state transition :
     * 				FROM								TO								condition
     *  HLSPlayStates.IDLE              	HLSPlayStates.PLAYING_BUFFERING     play()/play2()/seek() called
     *  HLSPlayStates.PLAYING_BUFFERING  	HLSPlayStates.PLAYING  				buflen > minBufferLength
     *  HLSPlayStates.PAUSED_BUFFERING  	HLSPlayStates.PAUSED  				buflen > minBufferLength
     *  HLSPlayStates.PLAYING  				HLSPlayStates.PLAYING_BUFFERING  	buflen < lowBufferLength
     *  HLSPlayStates.PAUSED  				HLSPlayStates.PAUSED_BUFFERING  	buflen < lowBufferLength
     */
    public class HLSNetStream extends NetStream {
        /** Reference to the framework controller. **/
        private var _hls : HLS;
        /** reference to buffer threshold controller */
        private var _bufferThresholdController : BufferThresholdController;
        /** FLV Tag Buffer . **/
        private var _streamBuffer : StreamBuffer;
        /** Timer used to check buffer and position. **/
        private var _timer : Timer;
        /** Current playback state. **/
        private var _playbackState : String;
        /** Current seek state. **/
        private var _seekState : String;
        /** current playback level **/
        private var _playbackLevel : int;
        /** Netstream client proxy */
        private var _client : HLSNetStreamClient;

        /** Create the buffer. **/
        public function HLSNetStream(connection : NetConnection, hls : HLS, streamBuffer : StreamBuffer) : void {
            super(connection);
            super.bufferTime = 0.1;
            _hls = hls;
            _bufferThresholdController = new BufferThresholdController(hls);
            _streamBuffer = streamBuffer;
            _playbackState = HLSPlayStates.IDLE;
            _seekState = HLSSeekStates.IDLE;
            _timer = new Timer(100, 0);
            _timer.addEventListener(TimerEvent.TIMER, _checkBuffer);
            _client = new HLSNetStreamClient();
            _client.registerCallback("onHLSFragmentChange", onHLSFragmentChange);
            _client.registerCallback("onID3Data", onID3Data);
            super.client = _client;
        };

        public function onHLSFragmentChange(level : int, seqnum : int, cc : int, audio_only : Boolean, program_date : Number, width : int, height : int, ... tags) : void {
            CONFIG::LOGGING {
                Log.debug("playing fragment(level/sn/cc):" + level + "/" + seqnum + "/" + cc);
            }
            _playbackLevel = level;
            var tag_list : Array = new Array();
            for (var i : uint = 0; i < tags.length; i++) {
                tag_list.push(tags[i]);
                CONFIG::LOGGING {
                    Log.debug("custom tag:" + tags[i]);
                }
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_PLAYING, new HLSPlayMetrics(level, seqnum, cc, audio_only, program_date, width, height, tag_list)));
        }

        // function is called by SCRIPT in FLV
        public function onID3Data(data : ByteArray) : void {
            var dump : String = "unset";

            // we dump the content as hex to get it to the Javascript in the browser.
            // from lots of searching, we could use base64, but even then, the decode would
            // not be native, so hex actually seems more efficient
            dump = Hex.fromArray(data);

            CONFIG::LOGGING {
                Log.debug("id3:" + dump);
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ID3_UPDATED, dump));
        }

        /** timer function, check/update NetStream state, and append tags if needed **/
        private function _checkBuffer(e : Event) : void {
            var buffer : Number = this.bufferLength;
            // Log.info("netstream/total:" + super.bufferLength + "/" + this.bufferLength);
            // Set playback state. no need to check buffer status if seeking
            if (_seekState != HLSSeekStates.SEEKING) {
                // check low buffer condition
                if (buffer <= 0.1) {
                    if (_streamBuffer.reachedEnd) {
                        // Last tag done? Then append sequence end.
                        super.appendBytesAction(NetStreamAppendBytesAction.END_SEQUENCE);
                        super.appendBytes(new ByteArray());
                        // reach end of playlist + playback complete (as buffer is empty).
                        // stop timer, report event and switch to IDLE mode.
                        _timer.stop();
                        CONFIG::LOGGING {
                            Log.debug("reached end of VOD playlist, notify playback complete");
                        }
                        _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_COMPLETE));
                        _setPlaybackState(HLSPlayStates.IDLE);
                        _setSeekState(HLSSeekStates.IDLE);
                        return;
                    } else {
                        // buffer <= 0.1 and not EOS, pause playback
                        super.pause();
                    }
                }
                // if buffer len is below lowBufferLength, get into buffering state
                if (!_streamBuffer.reachedEnd && buffer < _bufferThresholdController.lowBufferLength) {
                    if (_playbackState == HLSPlayStates.PLAYING) {
                        // low buffer condition and play state. switch to play buffering state
                        _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
                    } else if (_playbackState == HLSPlayStates.PAUSED) {
                        // low buffer condition and pause state. switch to paused buffering state
                        _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
                    }
                }
                // if buffer len is above minBufferLength, get out of buffering state
                if (buffer >= _bufferThresholdController.minBufferLength || _streamBuffer.reachedEnd) {
                    if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
                        CONFIG::LOGGING {
                            Log.debug("resume playback");
                        }
                        // resume playback in case it was paused, this can happen if buffer was in really low condition (less than 0.1s)
                        super.resume();
                        _setPlaybackState(HLSPlayStates.PLAYING);
                    } else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
                        _setPlaybackState(HLSPlayStates.PAUSED);
                    }
                }
            }
        };

        /** Return the current playback state. **/
        public function get playbackState() : String {
            return _playbackState;
        };

        /** Return the current seek state. **/
        public function get seekState() : String {
            return _seekState;
        };

        /** Return the current playback quality level **/
        public function get playbackLevel() : int {
            return _playbackLevel;
        };

        /** append tags to NetStream **/
        public function appendTags(tags : Vector.<FLVTag>) : void {
            if (_seekState == HLSSeekStates.SEEKING) {
                /* this is our first injection after seek(),
                let's flush netstream now
                this is to avoid black screen during seek command */
                super.close();
                CONFIG::FLASH_11_1 {
                    try {
                        super.useHardwareDecoder = HLSSettings.useHardwareVideoDecoder;
                    } catch(e : Error) {
                    }
                }
                super.play(null);
                super.appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK);
                // immediatly pause NetStream, it will be resumed when enough data will be buffered in the NetStream
                super.pause();

//                for each (var tag : FLVTag in tags) {
//                    CONFIG::LOGGING {
//                        Log.debug2('inject type/dts/pts:' + tag.typeString + '/' + tag.dts + '/' + tag.pts);
//                    }
//                }
            }
            // append all tags
            for each (var tagBuffer : FLVTag in tags) {
                try {
                    if (tagBuffer.type == FLVTag.DISCONTINUITY) {
                        super.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
                        super.appendBytes(FLVTag.getHeader());
                    }
                    super.appendBytes(tagBuffer.data);
                } catch (error : Error) {
                    var hlsError : HLSError = new HLSError(HLSError.TAG_APPENDING_ERROR, null, tagBuffer.type + ": " + error.message);
                    _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
                }
            }
            if (_seekState == HLSSeekStates.SEEKING) {
                // dispatch event to mimic NetStream behaviour
                dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, {code:"NetStream.Seek.Notify", level:"status"}));
                _setSeekState(HLSSeekStates.SEEKED);
            }
        };

        /** Change playback state. **/
        private function _setPlaybackState(state : String) : void {
            if (state != _playbackState) {
                CONFIG::LOGGING {
                    Log.debug('[PLAYBACK_STATE] from ' + _playbackState + ' to ' + state);
                }
                _playbackState = state;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYBACK_STATE, _playbackState));
            }
        };

        /** Change seeking state. **/
        private function _setSeekState(state : String) : void {
            if (state != _seekState) {
                CONFIG::LOGGING {
                    Log.debug('[SEEK_STATE] from ' + _seekState + ' to ' + state);
                }
                _seekState = state;
                _hls.dispatchEvent(new HLSEvent(HLSEvent.SEEK_STATE, _seekState));
            }
        };

        override public function play(...args) : void {
            var _playStart : Number;
            if (args.length >= 2) {
                _playStart = Number(args[1]);
            } else {
                _playStart = -1;
            }
            CONFIG::LOGGING {
                Log.info("HLSNetStream:play(" + _playStart + ")");
            }
            seek(_playStart);
            _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
        }

        override public function play2(param : NetStreamPlayOptions) : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:play2(" + param.start + ")");
            }
            seek(param.start);
            _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
        }

        /** Pause playback. **/
        override public function pause() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:pause");
            }
            if (_playbackState == HLSPlayStates.PLAYING) {
                super.pause();
                _setPlaybackState(HLSPlayStates.PAUSED);
            } else if (_playbackState == HLSPlayStates.PLAYING_BUFFERING) {
                super.pause();
                _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
            }
        };

        /** Resume playback. **/
        override public function resume() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:resume");
            }
            if (_playbackState == HLSPlayStates.PAUSED) {
                super.resume();
                _setPlaybackState(HLSPlayStates.PLAYING);
            } else if (_playbackState == HLSPlayStates.PAUSED_BUFFERING) {
                // dont resume NetStream here, it will be resumed by Timer. this avoids resuming playback while seeking is in progress
                _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
            }
        };

        /** get Buffer Length  **/
        override public function get bufferLength() : Number {
            return netStreamBufferLength + _streamBuffer.bufferLength;
        };

        /** get Back Buffer Length  **/
        override public function get backBufferLength() : Number {
            return _streamBuffer.backBufferLength;
        };

        public function get netStreamBufferLength() : Number {
            if (_seekState == HLSSeekStates.SEEKING) {
                return 0;
            } else {
                return super.bufferLength;
            }
        };

        /** Start playing data in the buffer. **/
        override public function seek(position : Number) : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:seek(" + position + ")");
            }
            _streamBuffer.seek(position);
            _setSeekState(HLSSeekStates.SEEKING);
            /* if HLS was in paused state before seeking,
             * switch to paused buffering state
             * otherwise, switch to playing buffering state
             */
            switch(_playbackState) {
                case HLSPlayStates.PAUSED:
                case HLSPlayStates.PAUSED_BUFFERING:
                    _setPlaybackState(HLSPlayStates.PAUSED_BUFFERING);
                    break;
                case HLSPlayStates.PLAYING:
                case HLSPlayStates.PLAYING_BUFFERING:
                    _setPlaybackState(HLSPlayStates.PLAYING_BUFFERING);
                    break;
                default:
                    break;
            }
            /* always pause NetStream while seeking, even if we are in play state
             * in that case, NetStream will be resumed during next call to appendTags()
             */
            super.pause();
            _timer.start();
        };

        public override function set client(client : Object) : void {
            _client.delegate = client;
        };

        public override function get client() : Object {
            return _client.delegate;
        }

        /** Stop playback. **/
        override public function close() : void {
            CONFIG::LOGGING {
                Log.info("HLSNetStream:close");
            }
            super.close();
            _streamBuffer.stop();
            _timer.stop();
            _setPlaybackState(HLSPlayStates.IDLE);
            _setSeekState(HLSSeekStates.IDLE);
        };

        public function dispose_() : void {
            close();
            _bufferThresholdController.dispose();
        }
    }
}

add useful / commented debug code

HLSNetStream.as: add useful / commented debug code