A Preloader Parent Script
The preloadNetthing() function, according to the Lingo Dictionary, "preloads a file from the Internet to the local cache so it can be used later without a download delay. " When you use this function, a network ID number is returned and current movie continues to play.
The preloadNetthing() function alone doesn't do anything other than start the download process. To see how the download is progressing, we can use the getStreamStatus(ID) function to see how many bytes have been downloaded, and we can use netDone(ID) to see if the network operation has finished. Ideally, we would like to call these functions every now and again. Using timeout objects are a good way to do this. Rather than storing the net ID in a global variable and have a bunch of scripts scattered around the place watching what is going on, we are going to create a neat, self-contained script to start a preload and monitor its progress. Consider the following parent script (called "Preloader"):
"Preloader" Parent Script (v.1)
property myNetID
on new (me, netAddress)
myNetID = preloadNetthing(netAddress)
aTimerObj = timeout(me.string).new(100, #Timer_CheckProgress, me)
end
on Timer_CheckProgress (me, aTimer)
finished = netDone(myNetID)
if finished then
put "All done!!"
aTimer.forget()
else put "Still downloading..."
end
With your movie running, create a new instance of this script in the message window like this:
script("Preloader").new("http://www.lingoworkshop.com/dcr/ldr_test.dcr")
So whats going on here? When you create a new object from this script, the first thing the new object does is call the preloadNetthing() function using the URL that was specified as a parameter. It stores the NetID that is returned by this function in a property called 'myID'. The next thing it does is create a timeout object. This timeout object will then call the #Timer_checkProgress method of this object every 100 milliseconds.
You will notice that the #new method of this script doesn't return the object's reference (the 'me' parameter) as is usually the case. For an object to stay alive, there needs to be a persistent reference to the object somewhere - otherwise Director assumes it is no longer needed and removed is from memory (in a process called 'Garbage Collection' - see the Illustrated OOP Primer for more details). In this example, the timeout object keeps a reference to the object so we don't need to store another reference anywhere else.
The timeout object will call the #Timer_CheckProgress method of the object every 100 milliseconds. When this method is called, the Preloader Object uses the NetDone(id) function to check whether the preload has finished. If it has finished, it forgets the timeout object using the reference to the timeout object that is passed as a parameter (timeout objects always send a reference to themselves as a parameter when they make their callback). When the preloader object forgets the timeout object, the timeout object is destroyed - and since the timeout object held the only reference to the preloader object, the preloaded object conveniently gets destroyed as well.
Ok - so far we have created a script to create neat, self-contained objects which will preload a file then destroy themselves when the preload is complete. The next step is to enable this object to interact with other objects.
Since we want this script to be as generic as possible (so we can re-use it in other projects with the minimum of fuss), we will create a 'callback system' where the Preloader Objects sends a "PreloadComplete" message to a list of listener objects. Consider this version of the Preloader Script:
"Preloader" Parent Script (v.2)
property myNetID, myListeners
on new (me, netAddress)
myListeners = []
myNetID = preloadNetthing(netAddress)
aTimerObj = timeout(me.string).new(100, #Timer_CheckProgress, me)
return me
end
on AddListener (me, obj)
if not(myListeners.getOne(Obj)) then
myListeners.add(obj)
end if
end
on Timer_CheckProgress (me, aTimer)
finished = netDone(myNetID)
if finished then
aTimer.forget()
call(#PreloadComplete, myListeners)
else put "Still downloading..."
end
In this version, the preload script now has a method for adding objects as 'Listeners' for events the preload will generate. This version also returns a reference to itself when the #new method is called (unlike the previous version). This reference is used by the creating object to assign listeners. For example, the following behaviour will create an instance of the Preloader and then add itself as a listener:
-- Behaviour to create a preloader object, and then add itself
-- as a listener of the Preloader
on beginSprite me
if the runMode = "Author" then clearcache()
urlToLoad = "http://www.lingoworkshop.com/Tutorials/Preloader/Main.dcr"
preloader = script("Preloader").new(urlToLoad)
preloader.AddListener(me)
end
on PreloadComplete (me)
alert "Behaviour has been told that the preload is All Done"
end
So far, the preloader script will preload a URL and make a callback to a list of 'listener' object when the preload is complete. The final step is to have the preloader report its ongoing status so that we can create a progress bar of some similar device to give the user some feedback. To do this, we can use the getStreamStatus(myNetID) function to get the current 'status' of the network operation. This function returns a property list which contains information such as #state (whether "connecting" or "In progress"), the bytes total and the bytes currently transferred. In this next version of the Preloader script, this information is used to determine the portion of the URL that has been downloaded.
This next version also has a method for 'Destroying' the object, effectively aborting a preload.
-- "Preloader" Parent Script (v.3)
-- This version will send a 'PreloadStatusUpdate' message every 100ms,
-- and a 'PreloadFinished' message when the preload is complete.
property myNetID, myListeners, myAbortFlag
on new (me, netAddress)
myListeners = []
myNetID = preloadNetthing(netAddress)
aTimerObj = timeout(me.string).new(100, #Timer_CheckProgress, me)
return me
end
on Destroy (me)
-- abort the operation
netAbort(myNetID)
myAbortFlag = true
-- empty the 'listener' list
myListeners = []
end
on AddListener (this, obj)
-- Any object that wants to receive callback messages from this
-- widget needs to add itself as a listener object.
if not(myListeners.getOne(obj)) then
myListeners.add(obj)
end if
end
on RemoveListener (this, obj)
-- Any object that wants to stop receiving callback messages from
-- this widget can remove itself from this widget's list of listeners.
if myListeners.getOne(obj) then
myListeners.deleteOne(obj)
end if
end
on Timer_CheckProgress (me, aTimer)
if myAbortFlag then
-- we have aborted the operation, so forget the timeout object
atimer.forget()
return 0
end if
finished = netDone(myNetID)
if finished then
errorNum = netError(myNetID)
aTimer.forget()
call(#PreloadFinished, myListeners, me, errorNum)
else
-- still preloading; check the current status
status = getStreamStatus(myNetID)
currentState = status.state
-- before sending the status, work out the portion downloaded
case (currentState) of
"InProgress":
if status.bytesSoFar > 0 then
if status.bytesTotal > 0 then
f = MIN(1.0, float(status.bytesSoFar)/status.bytesTotal )
else f = 0.50
else
f = 0
end if
"Complete":
f = 100
otherwise
f = 0
end case
-- add the fractionDone as a property to the statusList
status.addProp( #fractiondone, f )
-- inform the callback object of the current state and the % transferred
call(#PreloadStatusUpdate, myListeners, me, status)
end if
end
Further Refinements
We could add another method for getting error descriptions (based on the error number the NetError() function generates). However, this method - along with the methods for adding and deleting Listeners - would be generic to all netoperations (Post and GetNetText, downloadNetThing). So, rather than adding the method to the preloader script, we are going to create a 'Netop' base-class which can be an ancestor to all the network scripts.
-- "NetOp" parent script (v.1)
property myListeners
on new (me)
myListeners = []
return me
end
on Destroy (this)
myListeners.deleteAll()
end
on AddListener (this, obj)
-- Any object that wants to receive callback messages from this
-- widget needs to add itself as a listner object.
if not(myListeners.getOne(obj)) then
myListeners.add(obj)
end if
end
on RemoveListener (this, obj)
-- Any object that wants to stop receiving callback messages from
-- this widget can remove itself from this widget's list of listeners.
if myListeners.getOne(obj) then
myListeners.deleteOne(obj)
end if
end
on GenerateEventMessage(this, event, args)
call (event, myListeners, this, args)
end
on GetErrorDescription (me, errorCode)
case errorCode of
"OK", "", 0: e = "No error"
4: e = "Bad MOA class. The required Xtras are missing. "
5: e = "The required Xtras are improperly installed or not installed at all."
6: e = "Bad URL or the required Xtras are improperly installed. "
20: e = "Internal error. The browser detected a network or internal error."
4146: e = "Connection could not be established with the remote host."
4149: e = "Data supplied by the server was in an unexpected format."
4150: e = "Unexpected early closing of connection."
4154: e = "Operation could not be completed due to timeout."
4155: e = "Not enough memory available to complete the transaction."
4156: e = "Protocol reply to request indicates an error in the reply."
4157: e = "Transaction failed to be authenticated."
4159: e = "Invalid URL."
4164: e = "Could not create a socket."
4165: e = "Requested object could not be found (URL may be incorrect)."
4166: e = "Generic proxy failure."
4167: e = "Transfer was intentionally interrupted by client."
4242: e = "Download stopped by netAbort(url)."
4836: e = "Download stopped for an unknown reason, possibly a network \
error, or the download was abandoned."
otherwise
e = "Unknown error code"
end case
return e
end
Below is the final version of the 'Preloader' script that inherits from the above 'NetOp' class:
-- "NetOp.Preloader" Parent Script (v.5)
-- This version will inherit the AddListener, RemoveListener,
-- and GetErrorDescription() methods from the NetOp script.
property ancestor
property myNetID
on new (me, netAddress)
ancestor = script("NetOp").rawNew()
callAncestor(#new, me)
myNetID = preloadNetthing(netAddress)
aTimerObj = timeout(me.string).new(100, #Timer_CheckProgress, me)
return me
end
on Destroy (me)
netAbort(myNetID)
myNetID = VOID
callAncestor(#Destroy, me)
end
on Timer_CheckProgress (me, aTimer)
if voidP(myNetID) then
-- we have aborted the operation
atimer.forget()
return 0
end if
finished = netDone(myNetID)
if finished then
errorNum = netError(myNetID)
if stringP(errorNum) then errorNum =0
aTimer.forget()
me.GenerateEventMessage(#PreloadFinished, errorNum)
else
-- still preloading; check the current status
status = getStreamStatus(myNetID)
currentState = status.state
-- before sending the status, work out the portion downloaded
case (currentState) of
"InProgress":
if status.bytesSoFar > 0 then
if status.bytesTotal > 0 then
f = MIN(1.0, float(status.bytesSoFar)/status.bytesTotal )
else f = 0.50
else
f = 0
end if
"Complete":
f = 100
otherwise
f = 0
end case
-- add the fractionDone as a property to the statusList
status.addProp( #fractiondone, f )
-- inform the callback object of the current state and the % transferred
me.GenerateEventMessage(#PreloadStatusUpdate, status)
end if
end
Downloads
Example Movie can be downloaded here. It contains the scripts discussed in this tutorial and the next.
(Updated 25th June 05)