Progressive archive loading in Flex

There are few ways to download files in Flex, each one is good for specific case.

 

  In this post I want to share just one of those cases when we have specific constraints like download of files bundle by one download session. When it can be helpful? Encryption can be a case. Encryption is pretty expensive processing feature, especially if you are going to download a lot of files in one loading session.

 

  It's better to "zip" all that files in one file and to encrypt just to one archive file, than to send hundreds of files and to encrypt/decrypt files one-by-one. Not just for encryption case, but sometimes we prefer to download one archive file and to manage just one download session instead of many.

 

  Just to make life more interesting, let's take a case when we have a document of 800 pages, when every page is separate SWF file. And we want to show page 1 (MovieClip object) before page 800 will be downloaded. In that case one huge archive file supposed to be serious barrier in progressive loading. The solution is pretty simple, but a little bit tricky.

 

  I'm going to show an example with so archive (with case oriented structure). In general, gzip and tar formats allow you to implement progressive loading - extract objects from zip before its fully downloaded. You are free to create your own "myzip" format, in this example you can check how it's easy. In general we will use "myzip" archive as files container (swf is well compresses already) just to put all files together in one single data bundle:

 

GZIP from inside - http://en.wikipedia.org/wiki/Gzip

 

MyZIP inside:

1) Some header with common archive information, for example XML structure with various nodes and attributes that can be used further in your application. In file beginning we have 2 values - some magic number in offset "0" that takes MAGIC_NUMBER_LENGTH bytes, right after is metadata size value which takes METADATA_SIZE_LENGTH bytes. And next after metadata size the metadata document itself. We know from metadata size value till what byte to read metadata. Just read a ByteArray from URLStream

 

2) Like in real life, usually as we read more, we know more. So when we finished with metadata reading we know many things we didn’t before – timestamp, document version, number of files (pages) inside. Number of pages is critical parameter and it’s going to solve mostly serious problem – the order of pages. Soon we’ll see why so problem can appear.

 

3) The very next bit after metadata is a first bit of first chunk which contains first swf file. Every chunk contains

·         chunk type (CHUNK_TYPE_LENGTH bytes), this value can define the type of Object (casting type) we are going to load

·         chunk size (CHUNK_SIZE_LENGTH bytes), defines the range of bytes in URLStream ByteArray

·         the object itself, in our case swf file – the rest we will ignore

 

To download the archive we’ll use URLStream instance. To allow progressive pages loading we will use ProgressEvent.PROGRESS

 
//URLStream instance
private var loader:URLStream = new URLStream();
//Pages array instance
private var _pages:Vector.<DocPage> = new Vector.<DocPage>();

private function onCreationComplete():void
{			
   loader.addEventListener( ProgressEvent.PROGRESS, onLoaderProgress, false, 0, true );				
   loader.addEventListener( IOErrorEvent.IO_ERROR, onLoaderError, false, 0, true );			
   loader.addEventListener( HTTPStatusEvent.HTTP_STATUS, onLoaderHttpStatus, false, 0, true );			
	
   loader.load(new URLRequest("http://mySyte.com/myzip.myz"));
}

 


 onLoaderProgress handler parses currently downloaded bytes, check if next chunk is fully downloaded, and assembles MovieClips from ByteArray by Loader object.

Almost as simple as sounds. The only troubles are when SWF files have different sizes. It takes different (unpredictable) time to load MovieClips from swf files, so the addition of pages to pages array by Event.COMPLETE adds your MoviClips to array in unpredictable order.

The solution is to cause to array to care about itself. In other words, let’s declare DocPage class that extends UIMovieClip with Event.COMPLETE handler of Loader instances:

 

 

public class DocPage extends UIMovieClip
{
   private var _pageNumber:int;
	
   [Bindable]
   /**
   * Getter/Setter of pageNumber property 
   * @return 
   * 
   */		
   public function get pageNumber():int
   {
	return _pageNumber;
   }		
   public function set pageNumber(value:int):void
  {
	_pageNumber = value;
  }
		  	
  /**
  * Object constructor, initiates page number property 
  * @param _pageNumber
  * 
  */		
  public function DocPage(_pageNumber:int)
  {		
	super();
		
	this.pageNumber = _pageNumber;
  }
		
  /**
   * adds movieClip content into the object 
   * fires firstPageAdded 
   * @param e - event
   * 
   */		
   public function addContent(e:Event):void
   {
        var mc:MovieClip = (e.currentTarget as LoaderInfo).content as MovieClip;
        super.addChild(mc);
	mc.gotoAndPlay(1);
		
        // dispatch this event to show first page 
        //immediately after it has been loaded
        if (this.pageNumber == 0)
	{
	   this.dispatchEvent(new PageFromArchiveEvent(PageFromArchiveEvent.PAGE_ARRIVED,true));				
	}			
	//trace("Page added " + pageNumber + " ..." );
   }
}

 

Every element of array will handle swf loader by itself, so the array persists of pages order.

And following the ProgressEvent.PROGRESS handler that can be used in your application that progressively loads and shows the pages before the whole archive have being downloaded:

 

private function onLoaderProgress(e:ProgressEvent=null):void 
{				
	//trace ("porgress ..." + e);
	if (loader.bytesAvailable)
	{
		loader.readBytes(byteArray, byteArray.length);
	}				
	var bytesToRead:uint = byteArray.length;
				
	// read from byteArray just in case it's bigger than chunk size
	while ( nextChunkPosition <= byteArray.length && byteArray.bytesAvailable)
	{					
		//reading archive header
		if ((bytesToRead >= MAGIC_NUMBER_LENGTH + META_SIZE_LENGTH) && !gotArchiveHeader)
		{
			byteArray.position = MAGIC_NUMBER_LENGTH;
			metadataSize = byteArray.readUnsignedInt();
			gotArchiveHeader = true;
		} 
		
		//reading archive metadata
		 if (gotArchiveHeader && 
			!gotMetadata && 
			(bytesToRead >= MAGIC_NUMBER_LENGTH + META_SIZE_LENGTH + metadataSize))
		{
			byteArray.position = MAGIC_NUMBER_LENGTH + META_SIZE_LENGTH;
			metaData = new XMLDocument (byteArray.readUTFBytes(metadataSize));
								
			nextChunkPosition = MAGIC_NUMBER_LENGTH + META_SIZE_LENGTH + metadataSize;
			
			pagesInit(metaData.firstChild.attributes.pages);
			
			gotMetadata = true;
		} 
		
		//reading chunk type and size
		if (gotArchiveHeader && 
			gotMetadata && 	
			chunkFullyRead &&
			(bytesToRead >=  nextChunkPosition + CHUNK_TYPE_LENGTH + CHUNK_SIZE_LENGTH ))
		{					
		        // get chunk type, 1=SWF
			chunkTypeSwf = (byteArray.readUnsignedInt() == 1); 
												
			//get chunk size						
			currentChunkSize = byteArray.readUnsignedInt();
			currentChunkPosition = nextChunkPosition + CHUNK_TYPE_LENGTH + CHUNK_SIZE_LENGTH;
			nextChunkPosition = currentChunkPosition + currentChunkSize;
					
			//if not swf ingnore the data
			if (chunkTypeSwf) 
			{
				//pages.push(new UIMovieClip());						
				chunkFullyRead = false;
			}
			else
			{
				byteArray.position = nextChunkPosition;
			}
				
		} 
					
		//reading chunk if it's swf - 1=SWF
		if (chunkTypeSwf &&
		!chunkFullyRead &&
		(bytesToRead >=  nextChunkPosition))
		{
			//current chunck byte array
			var ba:ByteArray = new ByteArray();
			byteArray.readBytes(ba, 0, currentChunkSize);						
															
			try 
			{								
				var bytesLoader:Loader = new Loader();
				bytesLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, pages[currentPageToAdd].addContent);
				bytesLoader.loadBytes(ba);
				currentPageToAdd++;	

                                //last chunk		
                     		if (currentPageToAdd == pages.length - 1)
				{
					// get chunk type, 1=SWF
					chunkTypeSwf = (byteArray.readUnsignedInt() == 1); 
					
					//get chunk size						
					currentChunkSize = byteArray.readUnsignedInt();
								
					currentChunkPosition = nextChunkPosition + CHUNK_TYPE_LENGTH + CHUNK_SIZE_LENGTH;
					nextChunkPosition = currentChunkPosition + currentChunkSize;
								
					chunkFullyRead = false;
				}
				else
				{
					chunkFullyRead = true;
				}
			}
			catch (e:Error)
			{
				trace ("ERROR: " + e);
			}
								
		} 
					
	}
				
}

 

the method initializes fixed size array:

 

        private function pagesInit(totalPagesNum:int):void
	{
		var pageNum:int = 0;
				
		while (pageNum < totalPagesNum)
		{					
			pages.push(new DocPage(pageNum));
			
			pageNum++;
		}
		//add listener to show first page immediatelly after it's been loaded		
		pages[0].addEventListener("firstPageAdded", onFirstPage);
	}

 

hope, you will find this code useful

 

Regards,

Vlad

 

 

 

 

 

 

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com