Categories Displayed in Flash

Archive for September, 2007

AS3: Caching Sound, Error #2037: Functions called in incorrect sequence, or earlier call was unsuccessful.

Sound object is a single use item. Once it's been loaded you can't use it again, regardless if it's the same mp3 or a different one, or null, you'll get the cryptic error "Functions called in incorrect sequence"

Example: do this:

s = new Sound(req);

///do a bunch of stuff, and at some time later try to load a new mp3 in
s.load(req);

You will get

Error #2037: Functions called in incorrect sequence, or earlier call was unsuccessful.

For each new mp3 you need to create a new Sound object. Note that SoundChannel is like a playhead, more than one of them can be accessing the Sound data, so you can create chorus/echo effects by triggering play() multiple times at different time/offsets. Here's a sound player that cycles through 2 mp3s with no errors, and caches the results.

import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.events.*;
import com.troyworks.core.persistance.CacheEvent;

var mp3s:Array = ["test.mp3","BT - Satellite.mp3"];

var cache:Object = new Object();

playNext_mc.addEventListener(MouseEvent.CLICK, playSound);

var s:Sound;
var _channel:SoundChannel;

function getNextAudioURL():String {
var curURL:String = mp3s.shift();
mp3s.push(curURL);
return curURL;
}
function playSound(evt:Event = null) {
var audioSwfURL:String = getNextAudioURL();
trace("***************************************************************");
trace("playSound " +audioSwfURL);
//trace(" attempting to restore " + audioSwfURL + " into AUDIO");
try {
if (_channel != null) {
//_channel.stop(); //UNCOMMENT ME TO STOP THE PREVIOUS PLAYING TRACK
//s.load(); //won't work with progressive
//s.close(); //won't work with progressive
}
if (audioSwfURL != null && cache[audioSwfURL] == null) {

//////////// normal audio loading ////////////
var req:URLRequest = new URLRequest(audioSwfURL);
s = new Sound();
//speaker_mc.display_txt.text =".";
s.addEventListener(Event.COMPLETE, onSoundLoaded);
s.addEventListener(IOErrorEvent.IO_ERROR, onSoundFailedToLoad);
s.load(req);
}else{

//////////// use cached audio ////////////////
trace("hitting cache");
var cevt:CacheEvent = new CacheEvent(Event.COMPLETE);
cevt.target = cache[audioSwfURL];
onSoundLoaded(cevt);
}
} catch (err:IOError) {
trace(err.toString());
} catch (err:Error) {
trace(err.toString());
}
}

function onSoundFailedToLoad(Event:IOErrorEvent):void {
trace("onSoundLoaded **FAILED**");
//speaker_mc.display_txt.text = "!";
}
function onSoundLoaded(event:Event ):void {
//speaker_mc.display_txt.text = "";

var localSound:Sound = event.target as Sound;

////////////// parse the key mp3 name /////////////////

//NOTE: a cleaner approach way would to be a Proxy that passes arguments along with the event, created during //the listener

var url:String = localSound.url;
var a:Number = url.lastIndexOf("/");
var b:Number = url.lastIndexOf("\\");
var c:Number = Math.max(a,b);
var url2:String = url.substr(c+1, url.length);
trace("onSoundLoaded" + url2);

//////////// cache it ////////////////////////
cache[url2] = localSound;
_channel = localSound.play();
localSound.addEventListener(Event.SOUND_COMPLETE, onPlayComplete);

}
function onPlayComplete(evt:Event):void {
//if has more sounds play them else, move to next slide
//content_mc.play();
}
playSound();

Caching Sounds...or anything for that matter

Once you've loaded a Sound(Bitmap etc) it's in memory so if your reusing it (e.g. Sound FX) there's no need to reload it. Here you can use a  cache, using the name of the sound as a key, and a CustomEvent to mimic the loaded event, as the default Event target is readonly, so you can't mimic the call coming from the cache. The solution is to override it, and call the listeners directly if you can't redispatch using the original sound.  The advantage is the loading event, don't know the difference between the real or loaded. The cache might also be used for offline/online type activities. But of course be aware you're memory useage for caching like this can get large quickly.

package com.troyworks.core.persistance {
import flash.events.*;
public class CacheEvent extends Event {

private var _overriddenTarget:Object;

public function CacheEvent(type:String,bubbles:Boolean=false, cancelable:Boolean=false) {
super(type, bubbles, cancelable);
}
override public function get target():Object{
return _overriddenTarget;
}
public function set target(target:Object):void{
_overriddenTarget = target;
}

}

}

AS3: Reloading Images? Fix for Error #2025: The supplied DisplayObject must be a child of the caller.

I have an image rotator that reloads images into the same clip.  This took me a bit to figure out. I'm using the non-visible preloader technique earlier in the blog, but it's modified to not throw errors via calling loader.unload() prior to adding the content to the target clip, apparently there is some contention over who owns it. Here's the code.

dynamic public class IMGLoader extends MovieClip {
private var images:Array = ["focused.JPG","TroyWorks-80x80.jpg"];
protected var _loader:Loader;
public var loading_mc:MovieClip;
public var reload_mc:MovieClip;
public var lt_mc:MovieClip;
public var curClip:DisplayObject;

public function IMGLoader() {
super();
trace("IMGLOader " + reload_mc);
/////////////////////////////////////
_loader = new Loader();
configureListeners(_loader.contentLoaderInfo);
reload_mc.addEventListener(MouseEvent.CLICK, startLoad);
startLoad();

}
public function getNextImageURL():String{
var curURL:String = images.shift();
images.push(curURL);
return curURL;
}
public function startLoad(evt:Event = null):void{

///////////// show loading animation ///////////
loading_mc.visible=true;
loading_mc.play();
var url:String = getNextImageURL();
trace("startLoadofImage" + url);
var request:URLRequest = new URLRequest(url);
//var clip:DisplayObject = lt_mc.getChild(curClip);

_loader.load(request);
}
private function configureListeners(dispatcher:IEventDispatcher):void {
dispatcher.addEventListener(Event.COMPLETE, completeHandler);
dispatcher.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler);
dispatcher.addEventListener(Event.INIT, initHandler);
dispatcher.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
dispatcher.addEventListener(Event.OPEN, openHandler);
dispatcher.addEventListener(ProgressEvent.PROGRESS, progressHandler);
dispatcher.addEventListener(Event.UNLOAD, unLoadHandler);
}

private function completeHandler(event:Event):void {
trace("completeHandler: " + event);
var clip:DisplayObject = DisplayObject(LoaderInfo(event.target).content);
clip.width = lt_mc.width;
clip.height = lt_mc.height;
trace("clip.name " + clip.name);
_loader.unload();
if(lt_mc.numChildren >1 ){
//lt_mc.removeChild(clip);
lt_mc.removeChildAt(1);
}
lt_mc.addChildAt(clip, 1);
curClip = clip;
trace("clip Loaded at " + lt_mc.getChildIndex(curClip));
///////////// hide loading animation ///////////
loading_mc.visible=false;
loading_mc.stop();
}

AS3: recreating onLoad, MovieClip.addFrameScript part duex

Recreating onLoad with MovieClip.addFrameScript

AS2 had an onLoad event, called after the frame's children had been created, which is largely how custom components were able to manipulate the visual state for useful things like:

  • register listeners.
  • hiding and showing clips (especially when multiple clips are stacked on top of each other for different error messages)
  • jumping to a frame,
  • setting up modal dialogs/mouse opaque areas

In AS3 this doesn't work the same, and this bites you on the occassion you have an actionscript class is registered to a class via the linkageID, like that of the "Document class.
If you try it you get the warning:

Warning: 1090: Migration issue: The onLoad event handler is not triggered automatically by Flash Player at run time in ActionScript 3.0. You must first register this handler for the event using addEventListener ( 'load', callback_handler).

So you think, ok, easy enough. Just add an eventListener to load. Test movie and then nothing happens. Search the help docs, and you find zero references to 'load' event. Check DisplayObject and you find every other event (render, added, etc) than the one we are looking for. Leads to one of two possibilities. One the load event doesn't exist... or second the load event has already been called prior to the class constructor being called, so adding a reference to it won't actually do anything. I can't really tell, I've tried searching and running some tests, and can't get it to work... which leads me to the question, just how do I accomplish all those in items above?

Thankfully the fix is easy: addFrameScript in the constructor.

addFrameScript(0,onLoadHandler); //works!!!
// addEventListener("load", onLoadHandler); // doesn't work

While on the subject,  in order to access clips on the  timeline from the *.as they should be public variables like this:

  • public var my_btn:SimpleButton
  • public var my_playerControls:MovieClip

and your class should be dynamic e.g

dynamic public class MyView extends MovieClip

Keeping the scripts already on the timeline.

Since your replacing the script call when it hits frame0, whatever is on the timeline won't be called. Thankfully since MovieClip is a dynamic class, scripts on the timeline are appended to the prototype, thus available by this which refers to the prototype chain instead of the class inheritance (Flash is multiple inheritance!). This is useful when the timeline has some configuration data/setup on it, and this approach allows you to call it prior or after your onLoadHandler

public function MyMovieClipClass(){

super();
addFrameScript(0,onLoadHandler);
// addEventListener("load", onLoadHandler); //doesn't work
trace(" AAAA CONSTRUCTOR AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
function onLoadHandler():void{
trace("BBBBB onLoadHandler BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
trace("onLoadHandler " + currentFrame);

///pre frameactions

if(!this["frame1"]){
this["frame1"]();
}

//post frameactions

my_btn.addEventListener(.....

my_playerControls.gotoAndStop("buffering");

}

OUTPUTS

  • AAAA CONSTRUCTOR
  • BBBB onLoadHandler
  • CCCC frame1TimelineScripts

AS3: NaN as Nullable Numbers

One major change from AS2 was the removal of Numbers being nullable, this was useful for determining if something had been given a value or not, and setting it to a default. One workaround is using NaN, which can be used on the function initializer:

  1. function testNum(num:Number = NaN) {
  2. if (isNaN(num)) {
  3. trace(num + " no num");
  4. } else {
  5. trace("has num: " + num);
  6. }
  7. }
  8.  
  9. /////////// BEGIN TEST /////////////
  10. var numA:Number; //NaN
  11.  
  12. testNum(); // NaN no num
  13. testNum(numA); //NaN no num
  14. numA = -1;
  15. testNum(numA); //has num: -1
  16. numA = 0;
  17. testNum(numA); //has num: 0
  18. numA = 1;
  19. testNum(numA); //has num: 1

AS3: MovieClip.addFrameScript

MovieClip.addFrameScript is a very powerful way to decouple script from the timeline. This post help you understand how it works.

When movies are compiled (including the top level swf), if no class is provided. Flash creates one for you behind the scenes. 1 Class is created per 1 MovieClip, typically sharing the same name. It helps to understand how the conversion happens, to make best use of it, and writing code on the timeline in general.

  1. imports are only needed once in a timeline
  2. variables declared on a frame, end up as class variables,
  3. you can't redeclare variables on multiple frames.
  4. only one function is called per frame, this can be tweaked, overridden or amended with addFrameScript

You can't redeclare variables,

on frame1 and frame2 put:
var aName:String = "A";
will give you the following error:
1151: A conflict exists with definition aFrame in namespace internal.

however putting on Frame 1

var aName:String ="A"; //frame1

and on Frame2

aName = "B" //frame2

works fine.

MovieClip.addFrameScript

Is the undocumented glue between the timeline frame and the class's function ( if bound). There can only be one script/function bound to a frame using the built in addFrameScript, though an alternative approach is to poll currentFrame changing (via Event.ENTER_FRAME).

This binding happens via addFrameScript(frameNumber-1, functionToCall...); Note that the first argument is ZERO BASED (while normally the timeline is 1 based)
During the Fla.compilation our script gets put into a function like:

Class Test_fla{

function frame1():*{

var aName:String = "A";

}

function frame2():*{

aName = "B";
}
}

The important thing to note is the frame1...frameN naming, where N is the frame number 1-whatever, We can validate this by decompiling the swf, using the AbcDump utility, which takes our test.swf and outputs:

function Test_fla::frame1():* /* disp_id 0*/
{
// local_count=1 max_scope=1 max_stack=2 code_len=16
0 getlocal0
1 pushscope
2 findpropstrict trace
4 pushstring "onFrame1"
6 callpropvoid trace (1)
9 findproperty aFrame
11 pushstring "aFrame"
13 initproperty aFrame
15 returnvoid
}

So the end will be a frame to function mapping (which we can't see) like:

  • [0]= frame1()
  • [1]= frame2()

Note again the frameNumber is zero based, while the script is 1 based.

Consequences

which has 2 consequences,

  1. you can't name your own functions frameN if one already exists. This is the same error as compiling in the same *.as:
    function frame1(){

    }

    function frame1(){

    }

  2. you can call and over ride these functions,
    addFrameScript(0, overrideFrame1);
    function overrideFrame1():*{

    //do something before
    frame1();
    //do something after
    }

In addition the top level Class, will be shared across different scenes, but these can have on the timeline different configuration values (say which language to use), and when used with jsfl to publish all scenes, create a handful of starter files.

Conclusion

This is kick ass for completely separating a UI.swf from a Controller.swf Keeping designers working on the timeline, and coders out of the design.

For the coders, and others working in Flash, is the ability to use addFrameScript to perform actions when a frame is fully initialized and children are on stage, similar to onLoad() in AS2, where-as use of ENTER_FRAME gets called prior to children in that movieclip getting called...this is similar to onInit() in AS2. So for example you can have a button only on a particular frame and setup it's event listener to call your controller...in another swf, regardless of what the playhead is doing.

In someways I find this cleaner than watching using addedToStage, rendered etc.

REFERENCES:

[1] Senoculars post
[2] FlashGurus info as Senocular
[3] BIG SPACESHIP, Extending addFrameScript to use the frame labels instead of framenumbers... the same glue I'm using as well, though I'm more interested in events being generated, than script.

[4] an alternate approach on evilzug's journal using ENTER_FRAME


You should be aware if using that approach that when the framescript is called via the ENTER_FRAME, items in the frame may have not been added yet, so this approach can't be used to bind to objects on the timeline (e.g. to registerListeners to buttons), this the addFrameScript does not suffer from.

Here's an output with one on the timeline the other added via oizys's frameScript approach:


entered frame 3
onReachedInstructions instructions null <- no button yet, via modified addFrameScript
timeline reached instructions [object SimpleButton] <- normal addFrameScript
onReachedInstructions instructions [object SimpleButton] <- now the button exists

AS3: MetaData

I've been curious about the usefulness of MetaData in non-flex projects,  as documented in these sites, it can be used for persistance, and introspection, as the metadata shows up in the describeType XML. It does involve more steps so I'm not sure how appropriate this is to a normal workflow.
http://blog.gtuhl.com/2007/03/05/flex2-custom-metadata/trackback/

http://orangeflash.eu/wp-trackback.php?p=45

UI: ImageTransfomer

Based on some AS1.0 code from Peter Hall I think, it's been tweaked over many years.

I've added dynamic registration point based off the code here (with significant tweaks). You can play with an early version here.

(Either JavaScript is not active or you are using an old version of Adobe Flash Player. Please install the newest Flash Player.)

The tool is contextual, and follows whatever is selected.

The cool thing about the imageTransformer (will likely change names as cropping and masking is implemented) is it's a container for UI interaction, not a direct editor so it can be added or removed easily from other DisplayObjects like:

  1. import com.troyworks.controls.*;
  2. import flash.events.MouseEvent;
  3. import flash.events.KeyboardEvent;
  4.  
  5. var aimgT:ImageTransformer = new ImageTransformer();
  6. aimgT.setClipToManipulate(a_mc);
  7. aimgT.setUIControls(scale_btn, rotate_btn, move_btn, c);
  8. aimgT.initStateMachine();

This tool selection is based on Cogs and the Sketch version will be coming shortly, thus easily skinnable. I'm going to implement a Focus and SelectionManager prior to releasing as the demo shows them all vying for the same control in parrallel leads to undesired behavior.

Mouse Click Lifecycle

Events are broadcast in exactly this order:

  1. MouseDown
  2. MouseUP
  3. MouseClick

If you play with it, note that MouseClick is triggered without a regard to time. The user could hold down the button for a year, drag it over and out of the clip, and as long as it's released over the same clip it was over, it registers a click.  Often in gestural UI's this is not the desired behavior, as that's not really click like, say like Double Click, two Press/Release event within a window of time. This is pretty easy to solve just do something like:

public function onPress(event:MouseEvent= null):void{
isPressed = true;
clickTime = getTimer();
}

public function onClick(event:MouseEvent= null):void{
var exitTime = (clickTime + 200); //200MS is reasonable click time.
var curTime = getTimer();
trace("mouse Click " + clickTime + " " + exitTime);
if( curTime < exitTime){
//do something with the click tim
}

}

Handling releaseOutside

Another gotcha, was the removal of releaseOutside. The comments here somewhat cover it. Though for my own solution I ended up using rollOver and rollOut to register the off stage release/click listener, that when back over the interested clip, remove it.

public function onRollOut(event:MouseEvent= null):void{
trace("onRollOut**************************************" + _clip);
//onReleaseOutside();
_clip.stage.addEventListener(MouseEvent.MOUSE_UP, onReleaseOutside);//, true); //recommended didn't work
}
public function onRollOver(event:MouseEvent= null):void{
trace("onRollOver******************************************");
_clip.stage.removeEventListener(MouseEvent.MOUSE_UP, onReleaseOutside);}

The Great Divide

The Flash8 vrs Flash9 Chasm

Although just one number has changed from Flash8 to Flash9, so much has changed, they might as well be on separate continents. The canyon is the best metaphor I've found to help project managers, programmers and the like, understand the differences in upgrading, and communication gap in them talking to each other, as well as the difficulties if Flash8 and prior projects are continued to be invested in, how much harder it will be in the future. The paradigms are that different, and the rate of development at Adobe is increasing faster, so
in the future it may be even harder to get up to speed.

One swf to rule them all: Coding a single swf for both web and AIR runtimes.

As cool as AIR is, by default as soon as you link against flash.filesystem.*, you lose portability onto web.

Consider an app that takes a saves anything. Say a webcam photo manipulator. On the desktop it may be great to save the JPG locally, when running on the web it would post to the webserver. Ideally it's the same swf. Unfortunately if you run the swf with links to flash.filesystem.File you get an error:

VerifyError: Error #1014: Class flash.filesystem::File could not be found.
at global$init()

As mentioned on Big SpaceShip Labs this is a runtime errors not compile time errors, and since they are so low level you can't try/catch them. Error boxes is the last thing your viewers want to see as the first thing they hit you app!

  1. import flash.filesystem.*;
  2. import flash.system.Capabilities;
  3. import com.troyworks.io.*;
  4.  
  5. var file:File = File.userDirectory;
  6. var fileP:IFile = new FileProxy();
  7. trace(fileP.userDirectory.name);
  8.  
  9. trace("playerType: " + Capabilities.playerType);

However there are ways around this, but they do involve a few more steps.

  1. Creating and interface to the various File, and FileSystem, and programming to that interface
  2. Creating File/etc. Wrapper objects that use that same interface the AIR exposes.
  3. Putting all the (2) + AIR packages in a separate swf that is loaded at runtime,
  4. Detect if it's running in the AIR, and loading and using that library.

This same approach can be used to make the swf's persistance features work with other Projectors, e.g. BuddyAPI, Zinc, WebDAV, etc. Provided the Interface can be abstracted enough.
1. Creating the flash.filesystem.File Interface
This is a reasonable amount of work as the flash.filesystem.File doesn't conform to an public interface, as far as I can tell, and it's complicated as most of the calls return "File" so they to have be rewrapped. So something simple is:

  1. package com.troyworks.io {
  2.  
  3. public interface IFile {
  4.  
  5. function get userDirectory() : IFile;
  6. function get name():String;
  7. }
  8.  
  9. }

2. Creating the Library

You'll need two swfs. The library ("AirLib.swf") and the caller (OfflineOnline.swf).

AirLib.swf frame 1 has the following script

  1. import flash.filesystem.*;
  2. import flash.system.Capabilities;
  3. import com.troyworks.io.*;
  4. import flash.text.TextField;
  5.  
  6. var ClassReference:Class = getDefinitionByName("com.troyworks.io.FileProxy") as Class;
  7. var fileP:IFile = new ClassReference();
  8.  
  9. //var file:File = File.userDirectory;
  10. var _fileP:IFile = new FileProxy();
  11. trace("useDIrectoryName " + fileP.userDirectory.name);
  12. status_txt.appendText("useDIrectoryName " +fileP.userDirectory.name);
  13.  
  14. trace("playerType: " + Capabilities.playerType);

Clicking Debug should show the output in the Debug Output window and the textfield.

3. Detecting the AIR Player Runtime
this is pretty easy, just using the system.Capabilities:

  1. import flash.system.Capabilities;
  2.  
  3. trace("playerType: " + Capabilities.playerType);  //AIR = playerType: Desktop , FlashIDE = "External" Project = "Standalone"

4. Creating Library User

Then providing the loaded swf access to the callling swf via the LoaderContext and then later retrieving the class via getDefinitionByName

  1.  
  2. /////////////////////////////////////
  3. _loader = new Loader();
  4. var ldrContext:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);
  5.  
  6. configureListeners(_loader.contentLoaderInfo);
  7. _loader.addEventListener(Event.COMPLETE, completeHandler);
  8. var request:URLRequest = new URLRequest(url);
  9. _loader.load(request, ldrContext);
  10. <blockquote>function completeHandler(event:Event):void {
  11. trace("completeHandler: " + event);
  12.  
  13. var clip:DisplayObject = DisplayObject(ExternalLibLoader(event.target).loadedClip);
  14.  
  15. addChild(clip);
  16. var ClassReference:Class = getDefinitionByName("com.troyworks.io.FileProxy") as Class;
  17. var fileP:IFile = new ClassReference();
  18. status_txt.appendText(fileP.userDirectory.name);
  19.  
  20. }

Conculsion

At this point when run the Library user will selectively load the AIR library or not, depending on what environment it's loaded in. The output swf can be run in AIR 1.0, Flash Projectors, and the web player without giving errors and doing sensible persistence based on what options are available.

No code is available yet, I still have a lot of work to do to wrap the File and related classes.

« Previous Entries