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)

First published 23/05/2005