Flash: FDT and AIR 1.0
Recently upgraded from FDT Professional to FDT Enterprise. I was excited to see that AIR is now supported. Of course it's been out for several months, just haven't paid any attention to it. Updating since a few things have changed since AIR went 1.0
- Download + Install the AIR runtime (now released)
- Download the Flex SDK (now open sourced),
- unzip the Flex SDK to someplace reasonable e.g. C:\CodeProjects\AS3.0\Flex3.0.0.47\
- Go to 'Window>Preferences' then click on the 'AS3 Core Libraries' Tab, then the 'Add' button
- This will get you to the Define a New Path Variable
- Finally you'll want to update the Core Library for the package you are using via right clicking over the project and selecting 'properties' then the 'Change Core Library' button on the bottom right:
- After you select the Core Library you created earlier, it will update and rebuild all the projects, and here's what it will look like after:
- I typically add the CS3CoreComponents too, as I do lots of project that are Flash First instead of Flex Based.
- Past that follow the instructions starting with step 5. on adding the sample AIR resources folder,
- download and replace the HelloWorld Application Descriptor, else you will get a series of errors like here
- modify the HelloWorld to match whatever you are using.
- Open up the Build_AIR.xml for editing
- change the 'sdk_dir' to point to the Flex3 path on your harddrive you downloaded earlier,
- change the 'app_name' to whatever you called your app, e.g.
- Then follow steps 8 and on, to build the swf and package it for AIR, if successful, you should then get something that looks like this. Congratulations!.
If your curious about what else has changed since the original article check the devnet.
Adobe Updates AIR, for Flash CS3, “The application requires a version of the Adobe Integrated Runtime which is no longer supported”
Tried publishing in the Flash IDE to AIR this morning and it output...blank space. Left me scratching me head for a bit, tried another air app, and got this.
Turns out that AIR had been updated like yesterday. But this requires a few steps to fix Flash CS3's publish for AIR. Instructions are spread across here.
- http://www.adobe.com/support/flash/downloads.html#flashCS3
- http://labs.adobe.com/wiki/index.php/AIR:Flash_CS3_Professional_Update#Download_and_Install
Which was far more involved than I was expecting, you have to uninstalled some things manually, and then to execute a jsfl script to cleanup afterward. To compound things, the updates want Firefox to be closed down, which have the instructions on it.
After all that....my file still didn't publish as AIR. Weird as it is, renaming the file allowed it to publish again *scratches head*
Gripe.
I realize AIR is beta, and it offers some really amazingly cool features. But it bums me out a little that the few AIR apps I've got installed are now all broken, and it's unknown if they will be updated in a timely fashion, being mostly in the widget, experimental category. Uninstalling isn't hard but it is annoying.
I find it also a bad user experience, if I can't get to the help/about to of an oddball named AIR app, to remember where I downloaded the widget in the first place, due to the above dialog, which isn't particularly helpful either in the user resolving the situation (like where to download the latest version). It makes me less gung-ho about delivering AIR products, if whatever I release can break in the wild at any time at Adobe's discretion.
Why AMF Rocks and beats XML
Ted made a great summary of why AMF is Flash's file format of choice.
http://www.onflex.org/ted/2007/11/abcs-of-amf.php
While I've touted the benefits about persistence and serialization of native classes in previous posts (see persistance or serialization categories). Often XML is preferred for data interchange with servers or text editor compatible configuration. Here Ted had a gem in the comments, comparing why using XML inside of AMF over XML in a text file, rocks!
E4X is super handy for querying the data and returning collections after parsing to xml. That said XML is a native object and can be included in AMF3. The data is stored as a string zlib compressed and when deserialized becomes an XML object again.
bytes.writeBytes( new XML('') );
This gives you zlib compression and benefits of e4x in one go. zlib is very effective on xml and can compress documents as much as 20X, I have seen 300Kb XML files become 7K given tag redundency in the ASCII text.
The XML human readable is important as often for games and such that I build, but frequently it's doesn't make sense to build a configurator given the many excellent XML editors out there. In addition it's very easy for most servers to generate, where they can't generate AMF as easily. But there are two compelling benefits to going that route:
1) As the XML grows, compressing things by up to 20x can improve the responsiveness of the application (like startup of a website), and reduce network costs.
2) XML stored this way is native data type to Flash not a string...meaning it's already an XML accessible to E4X instead of a random string that has to be parsed into XMLNodes. Reconstituting the data out of the AMF stream still takes overhead, but it should be much faster than parsing it from interpreting tags and attributes from the plain text, as most the marshalling code is in C inside the player instead of actionscript. Read better experience for end users.
What occurs to me now is that either a very simple AIR app act in a similar fashion to Winzip for compressing/decompressing text for consumption by flash, or act as a simple text editor, just using a text field.
I'm sure that Adobe will open up the AMF API so that servers can generate raw AMF without using remoting or RMTP. This will open up the possibilities for CMS driven websites to prerender sites daily out of database requests.
Using ByteArray to Serialize AS3 Classes for AIR, Web and Everywhere!
When using AIR's FileStream to write AS3 classes to ByteArray, I've found that the same file format works for loading in via URLLoader, as well as what's uploaded using FTP. This isn't surprising, but wanted to test anyway.
What this means is that ByteArray is a groovy ubiquitous format for storing full classes and dependencies and references, via ByteArray.writeObject and ByteArray.readObject (provided registerClass is used), without nececessarily having to parse them manually though that is still suggested for versioning and class evolution. Meaning you can create a file using AIR , send that file to a friend in email, and have them open it up in a flash based webpage, and they operating in a webpage could Http POST the change back to you. The ByteArray can have rich complexity, classes, self references, collections, etc. This could be used to save whole game states (even massive multiplayer, NPC brains, etc). Or in my target case debugging outside other non-AIR runtimes to speed development.
How to save to the same directory the swf AIR is running from
This took me a bit to figure out. Primarily as I'm using AIR to write the files in Flash CS3 and it's not immediately apparent in the docs, how to target the 'self' directory of the swf. This isn't recommended in the docs (which I agree) but in the case I'm using it for it makes sense to do so. The API for AIR is here
-
-
// this is in url path
-
var file:File = File.applicationDirectory.resolvePath( "example.jpg" );
-
trace("file " + file.url + " " +file.nativePath ); //outputs app:/example.jpg, and "C:\SomeDirectory\Somepath\example.jpg"
-
-
var wr:File = new File(file.nativePath);
-
// Create a stream and open the file for asynchronous reading
-
var stream:FileStream = new FileStream();
-
stream.open( wr, FileMode.WRITE );
-
stream.write.....
-
stream.close();
-
This would be swf._url in AS2, or in AS3 DisplayObject.loaderInfo.url , as when using relative links in URLLoader to load whatever you're saving this will resolve properly in AIR. Turns out flash.filesystem.File.applicationDirectory is the droid you are looking for. It's not fully qualified (e.g. file:C:\somedirectory...) when traced it looks like:
"app-resource:/yourSwfNameGoesHere.swf"
unless you use the file.nativePath which is required in order to actually write to the directory. So you can use still this in old school string name parsing the swf name out, so you don't have to use File.applicationResourceDirectory if your looking to make a single swf for both AIR and Web use, and on the web, the AIR libraries won't be accessible.
Here's two handy scripts to help inspect ByteArray's, the first looks at the 1's an 0's, the other looks at the objects in the stream, using the getQualifiedClassName, and if it finds an Array or sub ByteArray attempts to inspect it.
/* converts byteArray to binary like
TraceBits--------------------
0 = 110
1 = 1011
2 = 1001000
3 = 1100101
*/
-
-
function traceBits(bA:ByteArray) {
-
trace("TraceBits--------------------");
-
for (var i:int = 0; i < bA.length; i++) {
-
bA.position = i;
-
-
var inte:uint = bA.readByte();
-
trace(i+ " = " + inte.toString(2));
-
}
-
}
-
-
function traceByteArray(bA:ByteArray) {
-
var o;
-
var cnt:int = 0;
-
var cnm:String;
-
-
var o2;
-
var cnt2:int = 0;
-
var cnm2:String;
-
try {
-
while (true) {
-
o = bA.readObject();
-
cnm = getQualifiedClassName(o);
-
trace( cnt++ + " " + o + " : " + cnm);
-
if (cnm == "flash.utils::ByteArray") {
-
try {
-
cnt2 = 0;
-
cnm2 = "";
-
while (true) {
-
o2 = o.readObject();
-
trace(" " + cnt2++ + " " + o2 + " : " + getQualifiedClassName(o2));
-
-
}
-
} catch (err:Error) {
-
}
-
} else if (cnm == "Array") {
-
trace("array contents...");
-
try {
-
var ar:Array = o as Array;
-
trace("ar " + ar);
-
trace(ar.join("\r"));
-
} catch (err:Error) {
-
trace(err.toString());
-
}
-
}
-
}
-
} catch (err:Error) {
-
}
-
}
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!
-
import flash.filesystem.*;
-
import flash.system.Capabilities;
-
import com.troyworks.io.*;
-
-
var file:File = File.userDirectory;
-
var fileP:IFile = new FileProxy();
-
trace(fileP.userDirectory.name);
-
-
trace("playerType: " + Capabilities.playerType);
However there are ways around this, but they do involve a few more steps.
- Creating and interface to the various
File, andFileSystem, and programming to that interface - Creating File/etc. Wrapper objects that use that same interface the AIR exposes.
- Putting all the (2) + AIR packages in a separate swf that is loaded at runtime,
- 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:
-
package com.troyworks.io {
-
-
public interface IFile {
-
-
function get userDirectory() : IFile;
-
function get name():String;
-
}
-
-
}
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
-
import flash.filesystem.*;
-
import flash.system.Capabilities;
-
import com.troyworks.io.*;
-
import flash.text.TextField;
-
-
var ClassReference:Class = getDefinitionByName("com.troyworks.io.FileProxy") as Class;
-
var fileP:IFile = new ClassReference();
-
-
//var file:File = File.userDirectory;
-
var _fileP:IFile = new FileProxy();
-
trace("useDIrectoryName " + fileP.userDirectory.name);
-
status_txt.appendText("useDIrectoryName " +fileP.userDirectory.name);
-
-
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:
-
import flash.system.Capabilities;
-
-
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
-
-
/////////////////////////////////////
-
_loader = new Loader();
-
var ldrContext:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);
-
-
configureListeners(_loader.contentLoaderInfo);
-
_loader.addEventListener(Event.COMPLETE, completeHandler);
-
var request:URLRequest = new URLRequest(url);
-
_loader.load(request, ldrContext);
-
<blockquote>function completeHandler(event:Event):void {
-
trace("completeHandler: " + event);
-
-
var clip:DisplayObject = DisplayObject(ExternalLibLoader(event.target).loadedClip);
-
-
addChild(clip);
-
var ClassReference:Class = getDefinitionByName("com.troyworks.io.FileProxy") as Class;
-
var fileP:IFile = new ClassReference();
-
status_txt.appendText(fileP.userDirectory.name);
-
-
}
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.
Flash on the Desktop: Zinc vrs Air vrs Director vrs F-in-Box
MDM Multimedia put there, with their comparison of AIR and Zinc, and I It's accurate but I feel an incomplete pitch. AIR doesn't really expect much more users than they currently expect with the flash plugin, and they expect much less from developers working in AIR, than Zinc or other lanaguages.
OTHER POINTS FOR AIR
- Integrated with Flex and Flash production tools. 1 step publish. Zinc, F-in-box, Director have many more steps and over the course of a project it adds up.
- integrated PDF and HTML and Javascript, with cross language communication.
- has a SQL database built into the runtime, no need to setup one.
- very quick installers and install from web.
- it can load/play anything flash can play, and will support the same codec as quictime, which shortly will probably represent 80% of the video on the web, youtube is already streaming their content for iPhone consumption.
The first two I think are very important. It opens up desktop development to web developers which has normally been an different tier of, and it does it much faster than Java or C+ workflows. Within minutes and only a paragraph of code one can create simple tools for text and image manipulation. Flash is great, but it's not necessarily ideal for text or layout heavy docs (e.g. tables). It can also be hard to source talent for. HTML on the other hand most kids in junior high can code.
F-in-Box's key points are, ability to stream movies and FLV from memory...nothing ever hits the disk. Write in Delphi, C, etc. But this requires someone skilled in one of those 'hard' languages, familiar with memory management.
Director is the lowest of my list at this stage as it has not kept up, but it has 3d support and has had stable cross platform desktop access for years. I think it's weak point are windowing, it's primarily kiosk mode or nothing.
Local Shared Objects…not so Shared. Cross Domain Projector Issues.
We have a a few Desktop Flash applications: One is a main application, the other an update utility for the main app. They use a Flash Cookie to find each other, as the main app could be installed anywhere (USB, C), so it writes to the cookie where it's installed at. The apps are based in Director using the Flash Xtra.
Say we start with a completely empty SharedObjects folder
Running the main app sets a cookie into a folder like
....Flash Player\#SharedObjects\BFYLPV7P\localhost\02000.sol
via a call like
var so : SharedObject = SharedObject.getLocal ("02000", "/");
where "/" should see the root of the Flash Cookies.
then say Pandora (or any other flash based website using cookies) runs it generates a new sandbox
....Flash Player\#SharedObjects\2S7GJDMQ\pandora.com
and then all future read requests are against that new '2S7GJDMQ' folder instead of the "BFYLPV7P" folder, and then neither the app or the updater, can see the old shared local object folder anymore. Which in the case of it containing lots of persistent configuration data, is the equivalent of clearing the Browser cache and forgetting everything.
Trying to understand how it's created, I can delete the contents of the shared Objects folder and then republish in the Flash IDE and get a new hash for the folder every time so it seems to be either random or time based. It's not as far as I can tell the behaviour documented, it's certainly not behaving as I would expect.
As mentioned here. It used to be that one could work around this via:
SharedObject.getRemote("my_so", "rtmp:/./..", "/");
sadly it no longer works in Flash 8, or 9.
I got this reply on the FlashCoders list:
>Movies running in different sandboxes cannot access shared objects of
>each other.
Yes, that makes sense, except for 2 things:
1) As far as I can tell, they should NOT be in separate sandboxes, they
use the exact same code, and are installed in next to the same folder. As far
as I can tell there is no way to specify the domain for a projector to get the
desired behavior.
2) It's also inconsistent. First run it WILL use the same sandbox as the Flash IDE/etc, then later something will trigger the generation of a new sandbox (install a second version of the app, or view pandora) then all further requests go against the new sandbox instead of the original. Meaning the Flash Player changes it's mind about which domain it thinks it belong to!
My best guess, likely due to it running in a projector without access to URL information, it's making stuff up.
Our first attempt at solving, required upgrading the Flash Xtra in both projectors to Flash8, apparently having one at Flash7 and the other Flash8 is enough to trigger the domain.
Also interesting is the Flash IDE never really seems to have this problem, presumably because it is using the Flash Player EXE as the handler for all swfs via the registry. So one potential workaround was firing a Flash Projector with the sole point in life to read/write the local Shared Object, with local connection object to read how flash sees it and then shut down.
Flash on the Desktop…everywhere. Flash CS3 supports AIR output
This week has been week of Desktop Based Flash. 1) Using Director + the Buddy API Xtra, 2) a client with a C++ and F-in-Box, and just finishing up save capability 3) AIR Local File Access for Flash
Testing applications inside of these projectors can really slow down application development as it's compile in flash, and then test in the projector.
Still I'm quite stoked to see that Flash IDE has an update to support building AIR apps in the IDE. This rocks for several reasons as many apps I make are based off XML configuration or could benefit from making tools to help generate the data. Here is a good post on actually using the update to create a hello world example.
This is after spending a week battling with bizarre Shared Object behavior. Memo to self, don't depend on SharedObjects for reliable desktop storage!
Serialization: IExernalizable and Evolving Classes
What is evolving a class? It's the inevitable:
- addition of a field
- change the superclass
- remove a field
- change the name of a field
- change a field to static (from non-static)
- change a field to transient (from non-transient)
- change the type of a field type (e.g. int to float)
As soon as you do any of the above whatever you've previously saved something to disk, be it local or remote will break upon loading back in.
This isn't common in most web based flash development which lives in isolation on a webpage and doesn't save anything, but introduce any sort of saving. With Desktop Flash apps (AIR, Zinc), and RIA's, offline online applications, it IS becoming something that is important.
There are 3 different strategies to serializing objects, only one is particularly useful for evolving classes.
- Direct:
writeObject(yourClassObject); - Customize with IExternalizeable: iterate over each attribute of your object and write each of those to the stream.
- Customize with IExternalizeable, iterate over each attribute of your object write it with a key to a Dictionary / HashTable.
Approach 1 is great for simple uses and first passes. However it doesn't scale well or at all. Approach 2 Is better but as the stream is sequential, this can make dealing with multiple versions difficult or impossible as you have to know the exact order in which to parse. Approach 3 is the best, it uses a Dictionary as an intermediate map, it's only a bit more code to write, and is a *whole* lot easier to parse. This is also the approach that Remoting uses when talking to the server with the ASObject, and in someways writing to disk is identical to writing to the filesystem.
Here is the list of imports:
-
-
import flash.utils.Dictionary;
-
import reflection.introspectable;
-
import flash.utils.IExternalizable;
-
import flash.utils.IDataInput;
-
import flash.utils.IDataOutput;
-
import flash.net.registerClassAlias;
Here is the version number, we'd need to increase everytime a significant data change has happened, and some of the static register class necessary to get the class to serialize and deserialize correctly:
-
-
public static const serialVersionUID:Number = 01;
-
private static const REG_DICT:* = registerClassAlias("flash.utils.Dictionary",Dictionary);
-
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
-
[RemoteClass(alias="reflection.IntrospectableObj")]
-
Here are the modified IExternalizable functions. Note that the read from the stream has to deal with all permutations for a given version to version, as this number of changes gets large, the normalization can be quite large, thankfully good architecture up front can prevent much of this.
-
-
///////////////////////////////////////////////
-
public function readExternal(input:IDataInput):void
-
{
-
var d:Dictionary = input.readObject();
-
var serialVersionUID:Number = d["serialVersionUID"] as Number ;
-
trace("readExternal " + serialVersionUID);
-
/////////////// COMMON TO ALL VERSIONS ///////////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2 || serialVersionUID == 3){
-
introspectable::introVarStr = d["introVarStr"]as String;
-
defVarStr = d["defVarStr"]as String;
-
-
pubByteArray =d["pubByteArray"] as ByteArray;
-
pubBitmapData= (d["pubBitmapData"] as BitmapDataWrapper).getBitMapData();
-
}
-
/////////////// COMMON TO TWO VERSIONS //////////////////////
-
if(serialVersionUID == 1 || serialVersionUID == 2){
-
}
-
-
if(serialVersionUID == 2 || serialVersionUID == 3){
-
-
}
-
if(serialVersionUID == 1 || serialVersionUID == 3){
-
-
}
-
///////////// COMMON TO ONE VERSION /////////////////////////
-
if(serialVersionUID == 1 ){
-
-
}else if(serialVersionUID == 2 ){
-
}else if(serialVersionUID == 3 ){
-
}
-
-
}
-
-
public function writeExternal(output:IDataOutput):void
-
{
-
trace("writeExternal....");
-
var d:Dictionary = new Dictionary();
-
d["serialVersionUID"] = serialVersionUID;
-
-
// name spaced var
-
d["introVarStr"] = introspectable::introVarStr;
-
d["defVarStr"] = defVarStr;
-
-
// this has already packed data
-
d["pubByteArray"] = pubByteArray;
-
-
// since BitmapData can't be retrieved, we've created a wrapper that can be.
-
d["pubBitmapData"] = new BitmapDataWrapper(pubBitmapData);
-
output.writeObject(d);
-
}
AS3 Serialization and Deserialization: ByteArray BitmapData registerClassAlias
Grr. Flash's registerClassAlias does not work with BitmapData (I wonder ?), so while you can save it to the ByteArray and wherever that can go... you can't get it back! As BitmapData complains about the constructor getting no args, presumeably that memory allocation is fixed upon the constructor.
This means you'll have to roll your own, which isn't too hard but it when having the BitmapData as members of other classes it might be a pain as each will have to serialize/deserialize the BitmapData using the IExternalizable.
Another option might have been overridding, but while BitmapData can be extended there isn't a no arg constructor, so the constructor can't really be overridden efficetively. A Proxy could be created for this.
Andrew has a good workaround, iterating over the pixels, though I've opted to write the width and height into the stream as well. You can use writeObject for the height and widht without problems (versus writing writeInt)
For additional details and others experiencing this problem: search terms "registerClassAlias BitmapData" "deserialize Bitmapdata"
- http://www.google.com/search?q=deserialize+BitmapData&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:official&client=firefox-a
- http://www.adobe.com/cfusion/webforums/forum/messageview.cfm?catid=641&threadid=1255888#4547075
- http://osflash.org/pipermail/red5_osflash.org/2007-April/011091.html
Solutions
- [1] http://www.cynergysystems.com/blogs/blogs/andrew.trice/BitmapData/SharedDesktop/srcview/
- [2] http://www.barncar.com/writejpg.mxml (sample code using corelib to save as Jpg)
- http://www.bytearray.org/?p=90 (a more flexible snapshot of video/swf to jpg using corelib, which is a completely different solution path)
- corelib is here http://code.google.com/p/as3corelib/
NOTE:
if you do need to use registerClassAlias you can do this trick:
private static const REG:* = registerClassAlias("reflection.IntrospectableObj",IntrospectableObj);
