Loading and Tracking Multiple Files at the Same Time (Part 2)
Three weeks ago, I started a two-part series on using a MovieClipLoader instance in a custom class to track the loading of multiple external files at the same time. When we left off, our custom MultiLoader class was complete enough to provide a decent amount of convenience: an addClip() method managed a specified external file, a target movie clip, and arbitrary event handlers; a loadClips() method set the mass download in motion. In the final paragraph, I said I’d follow up with a way to report the total group load progress. This is that follow-up.
The new stuff
The new functionality is essentially the result of the MovieClipLoader.getProgress() method and the EventDispatcher class, used to dispatch a custom onGroupProgress event. Because the full MultiLoader class was described last time, I’ll go into the changes first, then present the updated class at the end.
It makes sense to use the existing MovieClipLoader.getProgress() method to gather data on how many bytes have loaded for each file. Using that method means we can stay clear of the events for each MovieClipLoader instance in our _loaders array … after all, our class allows those events to be associated to functions from outside code, so they’re “already taken.” The problem with MovieClipLoader.getProgress() is that it incorrectly reports the bytesTotal property of its return object during the early stages of a file’s loading. An incoming JPG might weigh 28,342 bytes, but during its first few milliseconds, when only 218 bytes may have arrived, the getProgress() method reports 218 (or thereabouts) as the total number of bytes. After repeated testing, I wasn’t able to find a pattern to this value, which stumped me for a while.
Traditionally, we would want to compare bytesLoaded to bytesTotal in a loop to see if the former has reached the value of the latter. Under the circumstances, however, the value of bytesLoaded was consistently equal to bytesTotal for the first several fractions of a second, which meant I had to find a way to ignore this apparent “equality” until enough of the file had loaded. My solution — perhaps not the best — was to use an arbitrary _falseTotal value to do the ignoring.
When loadClips() is called, it now sets in motion a setInterval() loop as the last thing it does. This loop calls a new method, getGroupProgress() every 50 milliseconds. Here’s how this method looks:
private function getGroupProgress():Void {
var groupLoaded:Number = 0;
var groupTotal:Number = 0;
var i:Number = 0;
var len:Number = _clips.length;
for (i = 0; i < len; i++) {
groupLoaded += _loaders[i].getProgress(
_clips[i].target
).bytesLoaded;
groupTotal += _loaders[i].getProgress(
_clips[i].target
).bytesTotal;
}
if (_falseTotal) {
if (groupLoaded <= groupTotal) _falseTotal = false;
} else {
dispatchEvent(
{
type:"onGroupProgress",
target:this,
groupBytesLoaded:groupLoaded,
groupBytesTotal:groupTotal
}
);
if (groupLoaded >= groupTotal) clearInterval(_id);
}
}
What’s going on? By default _falseTotal is set to true: we’re assuming the first few reports of bytesTotal will be incorrect. Hold that thought. Two local variables, groupLoaded and groupTotal, are declared and initialized to zero. A for loop is set up just like in the loadClips() method; this time, however, the loop simply gathers the combined total of the return value of getProgress() as invoked on each MovieClipLoader instance in the _loaders array (see the previous article if this doesn’t immediately make sense).
The getProgress() method works like this:
A MovieClipLoader instance is used to invoke the method, and the corresponding target MovieClip instance is passed in as a parameter. Under normal circumstances, that might look like this:
mcl.getProgress(myClip);
… but in this case, the sample mcl and myClip instances are being supplied by array elements.
This method returns an object with two properties, bytesLoaded and bytesTotal. One way to gather those properties is to create an Object instance and set it to the return value …
var progress:Object = mcl.getProgress(myClip);
trace(progress.bytesLoaded);
trace(progress.bytesTotal);
… but a shortcut approach means we can do without the extra object:
trace(mcl.getProgress(myClip).bytesLoaded);
trace(mcl.getProgress(myClip).bytesTotal);
Once these data are gathered, the method checks if _falseTotal is true. By default it is, so it checks if groupLoaded is less than or equal to groupTotal. When that finally happens — because enough of any of the loading files has come in that the bytesTotal value reports correctly — _falseTotal is set to false and no longer needed. (Why less than or equal to? In cases where the files have already been cached, groupLoaded will be equal to groupTotal from the start, but we still need a way to turn off falseTotal. [Thanks to Cezet for helping me come to this realization.])
Next time getGroupProgress() is called by setInterval(), it goes straight into a call to the dispatchEvent() method (event dispatching is covered in this earlier blog entry). Finally, if groupLoaded is greater than or equal to grouptTotal, the setInterval() loop is cleared.
Pay careful attention to the additions to the class. We now have private _id and _falseTotal properties, in addition to the three Function properties required by EventDispatcher. We now have the import statement for EventDispatcher, as well as the initialize() method in our class’s constructor (also the initialization of _falseTotal to true). Finally, the new getGroupProgress() method.
Using the updated class
Usage can be identical to what it was before. The only difference may be if you want to handle the new onGroupProgress event, as dispatched by the class.
var ml:MultiLoader = new MultiLoader();
ml.addClip("file1.jpg", clipA);
ml.addClip("file2.jpg", clipB);
ml.addClip("file3.jpg", clipC);
ml.loadClips();
var listener:Object = new Object();
listener.onGroupProgress = function(evt:Object):Void {
trace(evt.groupBytesLoaded + "/" + evt.groupBytesTotal);
}
ml.addEventListener("onGroupProgress", listener);
The full code for the updated class
import mx.events.EventDispatcher;
class MultiLoader {
// PROPERTIES
private var _clips:Array;
private var _loaders:Array;
private var _id:Number;
private var _falseTotal:Boolean;
private var dispatchEvent:Function;
public var addEventListener:Function;
public var removeEventListener:Function;
// CONSTRUCTOR
public function MultiLoader() {
EventDispatcher.initialize(this);
_clips = new Array();
_loaders = new Array();
_falseTotal = true;
};
// METHODS
// Add Clip
public function addClip(
url:String,
target:MovieClip,
handlers:Object
):Void {
_clips.push(new Object());
var current:Number = _clips.length - 1;
_clips[current].url = url;
_clips[current].target = target;
_clips[current].handlers = handlers;
}
// Load Clips
public function loadClips():Void {
var i:Number = 0;
var len:Number = _clips.length;
var prop:String = "";
for (i = 0; i < len; i++) {
var listener:Object = new Object();
_loaders[i] = new MovieClipLoader();
for (prop in _clips[i].handlers) {
listener[prop] = _clips[i].handlers[prop];
}
_loaders[i].addListener(listener);
_loaders[i].loadClip(
_clips[i].url,
_clips[i].target
);
}
_id = setInterval(this, "getGroupProgress", 50);
}
private function getGroupProgress():Void {
var groupLoaded:Number = 0;
var groupTotal:Number = 0;
var i:Number = 0;
var len:Number = _clips.length;
for (i = 0; i < len; i++) {
groupLoaded += _loaders[i].getProgress(
_clips[i].target
).bytesLoaded;
groupTotal += _loaders[i].getProgress(
_clips[i].target
).bytesTotal;
}
if (_falseTotal) {
if (groupLoaded <= groupTotal) _falseTotal = false;
} else {
dispatchEvent(
{
type:"onGroupProgress",
target:this,
groupBytesLoaded:groupLoaded,
groupBytesTotal:groupTotal
}
);
if (groupLoaded >= groupTotal) clearInterval(_id);
}
}
}
Thanks to Mike for correcting a typo in my code!
March 20th, 2007 at 9:32 am
So far I have the first part of these multiloader posts working perfectly, they have been an emense help to me. I have however been having problems with this second part…
For some reason
setInterval(this, “getGroupProgress”, 50);
is only calling getGroupProgress once and stopping, also when i trace private vars such as _falseTotal and _id within getGroupProgress they show that they are undefined.
When i run the flash debuger it seems as though the single call to getGroupProgress is fixed but everything else is still undefined when traced within that particular function. Strange stuff.
Also, I noticed an error with your event object and made the following changes….
dispatchEvent(
{
onGroupProgress”,
target:this,
groupBytesLoaded:groupLoaded,
groupBytesTotal:groupTotal
}
);
should be…
dispatchEvent(
{
target:this,
type:”onGroupProgress”,
groupBytesLoaded:groupLoaded,
groupBytesTotal:groupTotal
}
);
March 20th, 2007 at 9:58 am
Mike,
First of all, thanks for noticing that typo! I corrected the article in both places. I really appreciate that! As it turns out, the
typeproperty can go before thetarget— or after … it really makes no difference; matter of preference — but what I had originally typed was flat wrong.As for your
undefinedissue … agreed, something strange is afoot. If_falseTotal(for example) is tracing asundefined, then it makes sense that the method would only execute once: if that property is out of scope, then presumably so are_loadersand_clips, which means the finalifstatement in that method probably tripping earlier and killing the loop. The solution depends on getting the method to scope itself to the class, which is what thatsetInterval()line does with that optional firstthisparameter.After correcting my typo, I simply copied the entire class from the article — from beneath the “The full code for the updated class” heading — and pasted that into a new file, then instantiated that in a timeline keyframe. I tested on three large JPGs from one of my servers, and it worked without a hitch.
March 20th, 2007 at 10:58 am
Well it would seem as though I am capable of typos as well =P After fixing it I got it working loading 20 JPGs from an XML file.
I belive I found 1 more problem, I noticed that it finished the onGroupProgress event before all of the images were loaded. This is what I think is going on let me know if this has any merit.
For some reason the getBytesTotal method is not returning the correct filesize right away (ok thats what _falseTotal is for) but when one loader starts reporting the correct total _falesTotal is set to false even if the other loaders are not yet reporting the correct total. If this first image finishes loading before the others start reporting the correct total then bytesTotal >= bytesLoaded and the interval is cleared without regard for the remaining images’ bytesTotal and bytesLoaded.
To test i traced groupBytesLoaded and groupBytesTotal in the onGroupProgress listener with clearInterval(_id); commented out of the class file.
The 1st step of the trace indicates that if clearInterval weren’t commented out the event would have stopped after being dispatched only once with a groupBytesTotal of 37900 nowhere near the actual total, which should be 1208365 bytes.
*** Trace - (Loaded / Total) ***
37900/37900
March 20th, 2007 at 11:03 am
*** The rest of the trace from my previous post (It seems to have been cut off) ***
…
37900/37900
87037/87037
133910/133910
133910/133910
133910/133910
133910/133910
133910/133910
362971/417460
362971/417460
467570/528928
520164/527033
520164/527033
520164/527033
520164/527033
520164/527033
576622/583491
574727/581596
579701/579701
627004/627004
623214/623214
909251/913387
970416/974552
1129250/1136885
1192891/1208365
1192891/1208365
1197027/1208365
1208365/1208365
March 20th, 2007 at 2:03 pm
OK I think I found a solution to the above noted problem. The following modifications to this class should ensure that the onGroupProgress event keeps broadcasting until the very end.
// Import the delegate class
import mx.utils.Delegate;
import mx.events.EventDispatcher;
class MultiLoader {
…
// Add these variables
private var _connections:Number;
private var _complete:Number;
…
// In the constructer add
_connections = 0;
_complete = 0;
…
// Add to the addClip() method. This allows the handlers to access members of the MultiLoader Class
…
_clips[current].handlers = handlers;
_clips[current].handlers.onLoadComplete = Delegate.create(this, closeConnection);
_clips[current].handlers.onLoadStart = Delegate.create(this, openConnection);
…
// Add these handlers
// When a file starts loading we increment _connections
private function openConnection(targ:MovieClip){
_connections++;
};
// File is done loading decrement _connections
// increment _complete
private function closeConnection(targ:MovieClip){
_connections—
_complete++;
};
…
// if there are no open connections and all existing clips
// are complete then we are done loading
// in getGroupProgress() replace “if (groupLoaded >= groupTotal) clearInterval(_id);” with
if (_connections == 0 and _complete == len) clearInterval(_id);
…
April 3rd, 2007 at 3:54 am
I made quite the same class for my projects, except for little things like I want to be able to get the bytesTotal and Loaded for the whole group to make a progress preload, and add a callback function for each clip loaded, etc.
But there is a main difference : I use a “public var onLoad:Function;” instead of the dispatchEvent to know when all is loaded. The main reason why I did that is that I didn’t even know we can make our own dispathEvent in Flash (I should read more docs
) but now I wonder what are the pros and cons of each method. Is one more quick ? more clean ? more powerful ? more… “waouh!” ?
April 6th, 2007 at 12:30 pm
Davel_x,
That’s the nice thing about programming in general … once you know your tools, the choice is yours.
To my thinking, dispatching your own event(s) means you don’t have to “hijack” existing events — they remain available (for you or other users of your class) to put to possibly good use elsewhere.
April 17th, 2007 at 3:14 pm
Nice piece of code. I was curious if there still exists a way for you to use onLoadInit and onLoadComplete with this class. Forgive my ignorance if there is.
April 18th, 2007 at 1:35 am
Tonight I am writing a class that will basically load/track multiple files, but it also has a queue manager built in.
The internet, being a grand unstable place that it is, drives me nuts that I can start downloading multiple files and I am never quite sure which one will start first, even if I put in a delay between loads. Also, if I load 5-6 .swf files, I stress the bandwidth and also some browsers limit the number of simultaneous connections.
My advice to others is to consider building a queue-loader class. You know what .swfs will load in what order. You can load each .swf as fast as the bandwidth allows, thus feeding the user .swf #1 while .swf #2 loads in the background. And because you only utilized one network connection, often you can establish a second one to stream an mp3 for sound - I find it nice as I can quickly provide background music.
Just a thought.
May 3rd, 2007 at 9:37 pm
Brian!
Thanks for the input.
Your approach makes great sense.
May 29th, 2007 at 8:39 pm
Hello - I’m a mid-level actionscripter, so I apologize in advance if my question is elementary. I am implementing your script for a project and am having the problem where the groupBytesTotal keeps increasing while clips are being loaded, as in mike’s trace output above. I also updated the script based on his suggestion that follows the trace posting - but same problem.
I can see that the clips are loading and the evt.groupBytesLoaded keeps increasing, but the total keeps increasing as well (it seems to jump when a new clip begins loading). “evt.groupBytesTotal” is supposed to return the total bytes of all clips being loaded, correct? Or am I missing something?
Help would be much appreciated.
This is the code that loads clips and reports progress:
import MultiLoader
var path:String = “http://fake/path/to/files/string/”
// note: in my movie I use an actual path which I am not showing here
// in order to keep my client’s server info confidential
var amountLoaded:Number;
var prcntLoaded:Number;
var ml:MultiLoader = new MultiLoader();
// slideNames is an array defined in the previous frame via loading XML info
// and slideNum is simply slideNames.length
for (i=0; i
May 29th, 2007 at 8:41 pm
Sorry - my post got cut off - here is the rest of the code:
for (i=0; i
May 30th, 2007 at 4:35 pm
I’m guessing it was the angle bracket that cut me off. Not sure how to do the code thing here. In any event I solved it by keeping track of the number of clips that are loaded rather than the bytes. Sorry for the multi posts.
June 4th, 2007 at 9:31 pm
great scripts here I see the same issue that keeps popping up, ie the error in total bytes reported at the beginning messing up the %. My work around was to get a total file size via php and to use that. Course, what I am doing is getting a list of all images with the name format of TN_xxx.jpg from my images directory, preload those with the %, then it goes into the main site and preloads the larger images in the background without any progress report, so it may not be the best solution for all.
Anyone have any ideas on how to get around this with a strictly flash solution? Problem with basing the % on files completed is that it can be both erratic (20 small files and 3 really big ones) and unresponsive (3 files = 3 updates, so it jumps 0%, 33%, 67%, 100%). Perhaps if it could feel out the jump, ie use the onProgress to set a variable just for that one loader of a default (flawed) bytesTotal, then when it reports a new higher value, that is the one to use. When all flawed sizes are replaced, then it starts dispatching the events. Not sure if the issue is consistently flawed though.
June 9th, 2007 at 7:16 am
Hi David, was scouring the net looking for a multiloader style class and ended up here once again! (Good to be back - you have a lot of useful tutorials and articles) I’m going to see if I can adapt this code to my needs - and try to do what I guess Brian is describing above….
A quick question though…shouldn’t this:
var listener:Object = Object();
be:
var listener:Object = new Object();
It jumped out at me as being something I’m not used to seeing… but I’m not sure if its right or wrong.
June 18th, 2007 at 10:01 pm
To thummper …
Your questions/observations are all good ones, and they bring to light the potential complexities involved in preloading more than just a few files at a time. A friend of mine is working on a set of class files to handle this sort of thing in a comprehensive manner, and I think he said he’d make his files freely available, so I’ll follow up with you (and here on this blog) as soon as I follow up with him.
To GWD …
Woops! You nailed it. I’m updating the article right now. Thanks for that!
June 24th, 2007 at 5:27 pm
I am implementing this class. It’s great and can make work easier, but i can’t solve one problem. The onGroupProgress function starts only while the files are not loaded yet. If you load them from hard drive or you reload the page (while swf is on a server and files to load are cached) this function doesn’t run. Any idea how to resolve it?
June 24th, 2007 at 11:13 pm
Cezet,
I’m actually running into a different problem. Your observation caused me to look into this class again — which I’ve been wanting to get back to, thanks to so much input from others! — and I noticed that the
onGroupProgressfunction, for me, works every time. The problem is that it doesn’t stop executing when files are cached.A quick solution, which I’ve corrected in the original article, is to change this line:
… which immediately follows
if (_falseTotal), to this:Let me know if that helps your situation at all.
June 25th, 2007 at 8:15 am
David, I’d like to share back with you my approach which is heavily based on what you started here. It isn’t finished yet - nor can I guarantee its bug free - although the core functionality means that I already have it integrated as part of a bigger project at the moment. You can see what’s not finished from my comments in the code, but it could be a week or two before I give it adequate attention to do finish off the extra features.
Meanwhile I hope what I’ve done is useful… I offer it up in the hope that it might be, in the same spirit of sharing that you exhibit here - without your blog article I’m pretty sure I wouldn’t have known where to start for this type of thing. I’ve tried to incorporate many of your reader’s comments into what I did as well. Please feel free to take it further if you are inclined to do so- if what I’ve done makes sense (or fix what I’ve done if parts of it can be improved). NB I’m a marketer who’s trying to be a programmer so please excuse any bizarre approaches I may have used.
I don’t see how I can post the AS code here… so I put it up on a googlepage:
http://gregdove.web.googlepages.com/multiloaderclass-wip
Or direct download of the actionscript file:
http://gregdove.web.googlepages.com/MultiLoader.as
(I hope that’s ok - let me know if not)
July 6th, 2007 at 8:24 am
Hi David
I’ve checked out your class, and I’ve seen that you are loading already defined images in runtime, is there also a way to use this class/system when loading images dynamically? In my case I’m loading an XML file and using a for loop I’m generating an array of images. Maybe you can help me out on this issue.
Thanks
Tiago
July 9th, 2007 at 6:12 pm
Tons of help guys.
I am trying to determine if this class is a good strategy for loading an intro and a site at the same time.
I am noticing that I can only access the movie clips via the events am I missing something? The variables that I setup as empty movie clips do not get set. Also is there any way to determine that a movie has finished playing?
Thanks Chris
July 10th, 2007 at 2:09 pm
To Tiago …
Hey, bro! I haven’t tried this dynamically, but in theory, it shouldn’t be a problem. Use the
MultiLoader.addClip()method in a loop, and you should be fine. I’ll follow up with you in a separate email.To Chris …
I’m not sure I understand all your questions, but I’ll do my best to reply. The movie clips you associate with the MultiLoader class should still be available, for sure. Whatever instance names they have before
addClip()is called should still be valid. I don’t know what you mean by “variables … as empty movie clips,” though.As for determining that a loaded SWF has finished playing, you could certainly, once the SWF has loaded, continuously check its
MovieClip._currentframeproperty against itsMovieClip._totalframesproperty until the former met the latter.July 27th, 2007 at 9:31 am
Hey guys!!
I think that you’ve done a fine job, and the class is working as it suppose to. My problem is this, I load the menu icons through an XML, but I don’t any of them to be displayed till the swf is fully loaded (ie the icons is fully loaded).
Thanks in Advance
George
July 30th, 2007 at 7:20 pm
George,
In that case, set the
MovieClip._visibleproperties of the relevant movie clip containers tofalse. When theonGroupProgressevent indicates thatgroupBytesLoadedhas gotten high enough, update the_visibleproperty for all totrue.August 8th, 2007 at 6:42 pm
Kudos David, this is very cool. I implemented something very similar to this in a project I am working on.
However, I wonder if you are seeing the same bug that I encounter when loading multiple files at the same time (~ 20-50 files) in Internet Explorer. To replicate the issue:
1. Create a Flash movie that loads 30 or so files at the same time
2. Create a web page with a link that opens the Flash movie in a *new* (pop-up) browser window.
3. In IE, load the web page with the link to the Flash movie.
4. Click on the link. The Flash movie should open in a new window.
5. Before the movie is finished loading all the files, close the browser window containing the Flash movie.
6. Repeat steps 4-5. The Flash movie will no longer load.
The IE Flash plugin does not seem to dispose of the multiple HTTP connections properly when the page is closed while the Flash movie is using those connections, and this somehow breaks the plugin.
I suspect that a queue manager, as Brian mentioned above, would solve this issue by limiting the number of HTTP connections that are open at one time. Brian, if you were able to implement this I would be interested to hear how you did it!
February 16th, 2008 at 1:10 pm
Hi, all i just want to say a huge thanks to David Stiller for making this great class!! great work!!
February 16th, 2008 at 6:38 pm
widdowmaker,
Thanks!
February 16th, 2008 at 6:55 pm
[Note on Rob’s post …]
I followed up with Rob personally, several months ago, because his post was thorough and well thought out. His idea of limiting the number of concurrent HTTP connections is a good one, but I haven’t taken the time yet to experiment with it.
July 2nd, 2008 at 1:44 pm
Hey everyone.
This looks very cool and I have been trying hard to make it work, however, it won’t work. I save .as file, but I have no idea how to import it… I am very new to this. Can anyone help me with this. Like really, explain. I mean, what I have to put in the main movieclip to import this class, cuz whatever I try, it would throw out that class can be defined outside the file. Maybe someone can send me example… damrudy@yahoo.com
Thank you so much.
Rudy
September 2nd, 2008 at 11:01 am
David -
I wanted to thank you for putting a huge smile on my face today! Awesome preloader for a pain in the butt site I’m trying to finish
You did it again
Thanks again!
Lauren
November 19th, 2008 at 5:08 pm
I’m just wondering how to reset the mcloader so that you can load in a new array of images?
For instance I have a gallery with multiple sections and I’m using a for loop to populate the array. How do I clear the mcloader so that I can use the for loop again and populate a new array to send?
November 19th, 2008 at 10:24 pm
To Rudy …
The nice thing is, it really doesn’t matter where you save the AS file, as long as you let Flash know where it is. You can do this by configuring your classpath setting to the folder that contains the class file. Here’s a freebie article I wrote for Community MX that should steer you in the right direction:
http://www.communitymx.com/content/article.cfm?cid=197DE
If you’re still stuck, by all means, write back! I realize you may have long since moved on, because it has taken me five months to reply (I do apologize for that!) but for what it’s worth, I’m here now.
I’ve been working on two Flash books this year, and they’re finally done and shipping, so I tend to have time in the evenings now.
To Lauren …
So glad to hear that!
To Mike …
Honestly, you might not have to clear anything out. Have you tried simply continuing to invoke the
addClip()method?I’m not sure exactly what you mean by “How do I clear the mcloader,” because this class creates a new
MovieClipLoaderinstance for each image file, but you could certainly add a new public method somewhere inside the body of the class. Call itreset(), maybe. Pop it right between theloadClips()andgetGroupProgress()functions:Notice that the function is public, just like
loadClip(), which lets you invoke it on yourMultiLoaderinstance. In theory, that should do it, because setting anArrayinstance to a new array. However, if you notice any odd behavior, you could take matters into your own hands and clear out each item in the_clipsarray, just for good measure: