Flash Media Server, Flex Builder 2, and ANT

Developing Flex 2 applications that talk with Flash Media Server is really fun.  I’m sure Adobe will tell you that Flex Data Services are the way to go, but that is overkill for a lot of the things I want to do.  If you want to create a set of widgets that support multiuser activity such as chat and sharing of real-time data, you don’t need the Enterprise data services that are FDS.  While Flash Media Server has real-time video and audio, I use it strictly for the real-time data via Remote SharedObjects , and the server-side scripting done in case-sensitive JavaScript.  The persistent data of Remote SharedObjects with a simple, yet flexible coding model makes them a very powerful way to get multi-user Flex 2 applications.  Combined with some simple server-side logic to maintain “data authority” as I like to call it, and you’ve got a really fun way to create real-time data, mult-user apps.

What are the alternatives?

XMLSocket ?  Parsing XML is more programmer friendly now with E4X, ECMAScript for XML , in ActionScript 3.  XMLSocket servers do not enforce XML, however… they are merely string servers that run on ports of 1024 or higher, thus making them flexible.  The cons?  They run on ports 1024 or higher… ONLY.  Client got a firewall?  Tough.  There are not a lot of big name XMLSocket hosting services.  Those that are out there are extremely expensive for what you get.  While the hosting might not be expensive, the license fee’s are.  A lot of the open source solutions aren’t finished.  Those that are assume you know PHP, Ruby, Perl, or Python well enough to finish it.  Assuming I could, where do I host these servers?  My host isn’t going to dig me having a PHP script stuck in a while loop…

FDS?  Sorry, can’t afford 60k for fun side projects that have the potential to go to extremely small commercial markets.

Some other TCP/IP binary server?  I’d like to create things today instead of getting burnt out writing something from scratch in ActionScript 3’s binary Socket API.

It’d be nice if Adobe would release a Remote SharedObject server, complete with server-side JavaScript on a totally different pricing and license scheme than Flash Media Server or Flex Data Services are.  There are a lot of use developing mulit-user Flash Player applications on FMS that have no need for real-time streaming video and audio.  I think the same crackheads that ran pre-Flex 2’s business unit took over Flashcom’s.

At any rate, there is no denying FMS is elegant to code with.  With a little work via the Observer pattern , you can implement FMS server-side components into Cairngorm 2 .  For ever server-side component you create, you merely create a client side equivalent as an Observer.  It listens for NetConnection or Remote SharedObject sync events and dispatches events.  These events run Commands which in turn set data… just like normal.  If your Views are bound to said data, boom, you’ve got a nice, clean separation.

One thing that sucks about the work flow, though, is:

  • you have to redeploy your .asc files, your server-side code (basically JavaScript written server-side components)
  • you have to restart your app via the Communication App Inspector, basically a small app written in Flash
  • you then toggle back to Eclipse (Flex Builder 2 )

That sucks.  The better way is to use some ANT tasks I’ve created along with a special SWF.  This SWF connects to the FMS Admin server, reloads the app, and then immediately quits.  I go into more detail below, or you can just download the source.  You could re-write the ActionScript 2 into ActionScript 3, but AS2 is just quicker than AS3 for this type of stuff.

Bottom line, you just double-click an ANT task in Eclipse, it does all that for you, and you don’t have to leave Eclipse.  That’s so hot!

First, the ANT tasks.

<project name=”DnDTools” default=”Deploy FCS” basedir=”.”>
   
    <description>
    by Jesse R. Warden
    jesterxl@jessewarden.com
    https://www.jessewarden.com
    jesse@universalmind.com
    http://www.universalmind.com
   
    A series of tasks to work with Flashcom.
    ************************************************   
       
    Start Flashcom will write out a bat file, and run it to start
    Flashcom as well as the Flashcom Admin service.
   
    Stop Flashcom stops both the Flashcom service and the
    Flashcom Admin service.
   
    Deploy FCS does a few things:
    * copies your .asc files to the FCS applications directory
    * writes a configuration XML file for the FCS App Reloader
    * runs the FCS App Reloader to reload your application instance on the FCS server
   
    </description>
   
    <!– ************************************************ –>
    <!– set global properties for this build –>
    <property name=”bin” value=”bin” />
    <property name=”localFCSDir” value=”../flashcom”  />
    <property name=”deployFCSDir” value=”C:\Program Files\Macromedia\Flash Communication Server MX\applications\fcsappdirectory” />
    <property name=”FCSAppReloader” value=”FCS_App_Reloader.exe” />
    <property name=”FCSAdminHost” value=”localhost” />
    <property name=”FCSAdminPort” value=”1111″ />
    <property name=”FCSAppName” value=”fcsappdirectory” />
    <property name=”FCSAdminUser” value=”myUsername” />
    <property name=”FCSAdminPassword” value=”myPassword” />
    <property name=”appWaitTime” value=”3″ />
    <property name=”bat” location=”bat” />
   
    <!– Copy files into the FCS app directory, write a config xml, and reload an FCS app –>
    <target name=”Deploy FCS”>
       
        <!– ************************************************ –>
        <!– Copy new FCS files to the applications directory –>
        <echo message=”Copying Flashcom files to application directory…” level=”info” />
        <copy file=”${localFCSDir}/Application.xml” todir=”${deployFCSDir}” />
        <copy file=”${localFCSDir}/asc/main.asc” todir=”${deployFCSDir}” />
        <copy file=”${localFCSDir}/asc/Chat.asc” todir=”${deployFCSDir}” />
       
        <!– ************************************************ –>
        <!– Write the config xml file for the FCS Reloader App –>
        <echo message=”Writing FCSAppReloader configuration xml file…” level=”info” />
        <!– Host –>
        <echo message=”&lt;config host=’${FCSAdminHost}’ “
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
        <!– Port –>
        <echo message=”port=’${FCSAdminPort}’ “
            append=”true”
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
        <!– App Name –>
        <echo message=”appname=’${FCSAppName}’ “
            append=”true”
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
        <!– Username –>
        <echo message=”username=’${FCSAdminUser}’ “
            append=”true”
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
        <!– Password –>
        <echo message=”password=’${FCSAdminPassword}’ “
            append=”true”
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
        <!– Hang Time –>
        <echo message=”hangtime=’${appWaitTime}’ /&gt;”
            append=”true”
            file=”${localFCSDir}/fcs_app_reloader_config.xml” />
       
        <!– ************************************************ –>
        <!– Launch the FCS Reloader App –>
        <echo message=”Re-loading the FCS application…” level=”info” />
        <exec executable=”${FCSAppReloader}”
            dir=”${localFCSDir}/”
            spawn=”false”
            resolveexecutable=”true” />
       
       
    </target>
   
    <target name=”Start Flashcom”>
       
        <echo message=”Starting Flashcom…” level=”info” />
        <mkdir dir=”${bat}” />
        <echo file=”${bat}/start_fcs.bat”>%SYSTEMROOT%\system32\net.exe start &quot;FlashCom&quot;
               
        </echo>
        <exec executable=”start_fcs.bat”
            dir=”${bat}”
            spawn=”false”
            resolveexecutable=”true” />
       
    </target>
   
    <target name=”Stop Flashcom”>
       
        <echo message=”Stopping Flashcom…” level=”info” />
        <mkdir dir=”${bat}” />
       
        <echo file=”${bat}/stop_fcs.bat”>%SYSTEMROOT%\system32\net.exe stop &quot;FlashCom&quot;
       
        </echo>   
        <echo message=”%SYSTEMROOT%\system32\net.exe stop &quot;FlashComAdmin&quot;”
            append=”true”
            file=”${bat}/stop_fcs.bat” />
       
        <exec executable=”stop_fcs.bat”
                    dir=”${bat}”
                    spawn=”false”
                    resolveexecutable=”true” />
       
    </target>
   
</project>

Now, the ActionScript 2 for the FCS Reloader App:

/*

FCS App Reloader
by Jesse R. Warden
jesterxl@jessewarden.com
http://www.jessewarden.com
jesse@universalmind.com
http://www.universalmind.com

This is release under a Creative Commons license.
More information can be found here:

http://creativecommons.org/licenses/by/2.5/

v 1.0.0
This app connects to your FCS server, and reloads
an application instance.  The typical use case is to
launch this app from ANT in Eclipse.

The alternative is to:
- toggle to the Communication App Inspector
- click the app instance and click "reload"
- if it's not there, type in it's name, and then click "load"
- toggle back to Eclipse

Now, you can just click once on your ANT task, and you're done!

This API assumes Flash Communication Server 1.0 and Flash Player 6.
I tested this in Flash Player 8, and the API should work
in Flash Media Server 2 per the livedocs.

*/
import mx.utils.Delegate;

function init()
{
	Stage.align = "TL";
	Stage.scaleMode = "noScale";
	Stage.addListener(this);
	
	createTextField("debug_txt", 0, 0, 0, 100, 100);
	debug_txt.background = true;
	debug_txt.backgroundColor = 0xFFFFFF;
	debug_txt.border = true;
	debug_txt.borderColor = 0x000000;
	debug_txt.html = true;
	debug_txt.multiline = true;
	debug_txt.selectable = true;
	debug_txt.wordWrap = true;
	var fmt:TextFormat = new TextFormat();
	fmt.font = "Verdana";
	fmt.size = 11;
	debug_txt.setTextFormat(fmt);
	debug_txt.setNewTextFormat(fmt);
	onResize();
	
	debugHeader();
	debug("Loading config xml...");
	
	NETCONNECTION_CALL_SUCCESS = "NetConnection.Call.Success";
	CONFIG_XML_FILE = "fcs_app_reloader_config.xml";
	
	TIMEOUT_WIDTH = 100;
	TIMEOUT_HEIGHT = 12;
	config_xml = new XML();
	config_xml.ignoreWhite = true;
	config_xml.owner = this;
	config_xml.onLoad = Delegate.create(this, onConfigLoaded);
	config_xml.load(CONFIG_XML_FILE);
}
function onConfigLoaded(success:Boolean):Void
{
	debugHeader();
	if(success == true)
	{
		debug("Config XML loaded, parsing...");
		var at:Object 		= config_xml.firstChild.attributes
		theHost				= (at.host == null) ? "localhost" : at.host;
		thePort				= (at.port == null) ? "1111" : at.port;
		theAppName 			= at.appname;
		theUsername 		= at.username;
		thePassword			= at.password;
		theHangTime			= (at.hangtime == null) ? 3 : parseInt(at.hangtime);
		if(isNaN(theHangTime) || theHangTime < 0 || theHangTime == null) theHangTime = 3;
		var tabs:String = "\t\t\t\t";
		debug("host: " 			+ theHost);
		debug("port: " 			+ thePort);
		debug("app name: " 		+ theAppName);
		debug("user: " 			+ theUsername);
		debug("password: " 		+ thePassword);
		debug("hang time: " 	+ theHangTime);
		
		connect();
	}
	else
	{
		debug("Failed to find " + CONFIG_XML_FILE + ", aborting.");
		lightFuse();
	}
}
function connect()
{
	debugHeader();
	debug("Connecting to FCS Admin...");
	
	nc = new NetConnection();
	nc.owner = this;
	nc.onStatus = function(info)
	{
		if(info.code == "NetConnection.Connect.Success")
		{
			debugHeader();
			debug("Successfully connected!");
			this.owner.onConnected();
		}
		else
		{
			debugHeader();
			debug("Problem connecting to FCS Admin, aborting.");
			this.owner.lightFuse();
		}
	};
	nc.connect("rtmp://" + theHost + ":" + thePort + "/" + theAppName,
		   theUsername,
		   thePassword);
}

function onConnected()
{
	//getActiveInstances();
	reloadApp("flex2test");
}
function reloadApp(str:String)
{
	debugHeader();
	debug("Reloading app...");
	nc.call("reloadApp",
		      new Responder(this,
				    reloadApp_result,
				    reloadApp_fault),
				    str);
}
function reloadApp_result(o)
{
	debugHeader();
	if(o.code == NETCONNECTION_CALL_SUCCESS)
	{
		debug("App successfully rebooted, exiting.");
	}
	else
	{
		debug("Failed to reboot the app!");
		debugProps(o);
		debugProps(o.level);
		debugProps(o.error);
		debugProps(o.code);
		debug(o.description);
		debugProps(o.description);
		debug("Exiting.");
	}
	
	//debugProps(o);
	lightFuse();
}
function reloadApp_fault(o)
{
	debugHeader();
	debug("reloadApp_fault");
	debugProps(o);
	lightFuse();
}

/*
function getActiveInstances()
{
	debugHeader();
	debug("getActiveInstances...");
	nc.call("getActiveInstances",
	      new Responder(this,
			    getActiveInstances_result,
			    getActiveInstances_fault));
}

function getActiveInstances_result(o)
{
	debugHeader();
	debug("getActiveInstances_result");
	if(o.code == NETCONNECTION_CALL_SUCCESS)
	{
		debug(o.data.length + " active instances.");
		debugProps(o.data);
	}
}

function getActiveInstances_fault(o)
{
	debugHeader();
	
	debug("getActiveInstances_fault");
	debugProps(o);
}
*/
function lightFuse()
{
	nc.onStatus = null;
	nc.close();
	nc = null;
	
	createEmptyMovieClip("timeout_mc", 1);
	timeout_mc.st = getTimer();
	timeout_mc.timeout = 3; // seconds
	timeout_mc.onEnterFrame = function()
	{
		var ct:Number = getTimer();
		var et:Number = ct - this.st;
		if(et > (this.timeout * 1000))
		{
			this._parent.boom();
		}
		else
		{
			var p:Number = et / (this.timeout * 1000);
			var w:Number = TIMEOUT_WIDTH * p;
			
		}
		timeout_mc.clear();
		timeout_mc.lineStyle(0, 0x000000);
		timeout_mc.lineTo(TIMEOUT_WIDTH, 0);
		timeout_mc.lineTo(TIMEOUT_WIDTH, TIMEOUT_HEIGHT);
		timeout_mc.lineTo(0, TIMEOUT_HEIGHT);
		timeout_mc.lineTo(0, 0);
		
		timeout_mc.moveTo(1, 1);
		timeout_mc.beginFill(0x666666);
		timeout_mc.lineTo(w, 1);
		timeout_mc.lineTo(w, TIMEOUT_HEIGHT - 1);
		timeout_mc.lineTo(1, TIMEOUT_HEIGHT - 1);
		timeout_mc.lineTo(1, 1);
		
		timeout_mc.endFill();
	};
	timeout_mc.onEnterFrame(); // draw immediately
	onResize();
}
function boom()
{
	delete timeout_mc.onEnterFrame;
	timeout_mc.removeMovieClip();
	fscommand("quit");
}

function onResize()
{
	debug_txt._x = 0;
	debug_txt._y = 0;
	debug_txt._width = Math.max(12, Stage.width);
	debug_txt._height = Math.max(12, Stage.height);
	
	var sx:Number = Math.max(6, Stage.width);
	var sy:Number = 6;
	timeout_mc._x = sx - (timeout_mc._width) - 6;
	timeout_mc._y = sy;
}
function debug(o)
{
	trace(o);
	debug_txt.htmlText += o + "";
}

function debugHeader()
{
	//debug("---------------");
}

function debugProps(o)
{
	if(o instanceof Array)
	{
		var len:Number = o.length;
		for(var i:Number = 0; i<len; i++)
		{
			trace(o[i]);
		}
	}
	else
	{
		for(var p in o)
		{
			debug(p + ": " + o[p]);
		}
	}
}

init();


Source Files – ZIP

Shared Libraries Lazy Load & TamperData

I’ve been using Shared Libraries for years, and only found out yesterday that they lazy load.  Meaning, they load on the fly.  I’ve made preloaders in the past for SharedLibraries, but wasn’t realizing that my “usage” of an asset was the thing actually triggering the load. It worked so I just moved on with life.

A Shared Library is a SWF that has symbols in it specifically made to be shared in multiple FLA’s.  That way if you have an app that has multiple SWF’s, they can all share the same asset.  This works great for large, internal videos, fonts, and sounds.

We had to do it because we have a design that uses 5 fonts.  At an average of 12k to 20k per font, you can see the need to ensure that all 32 or so SWF’s all share the same font.  We bring in the font symbols as imported for shared libraries, and viola.  The path that they import from, the framework.swf, is problematic since it’s hard coded into the SWF.  You can change it by right clicking on the symbol in the library and selecting “Linkage…”, but that’s a pain in the ass if you want to test locally vs. deployed to site.

No url is hard coded anywhere since we are using an Akamai load-balancer so these are the only exception per-se.  Since they are relative, they still respect the load balancer.  We’ve used JSFL to make our testing vs. deploy scenario transparent; the scripts update the Shared Font paths based on whether you are testing or deploying.

Anyway, in testing, my co-worker Tony showed me how to see what the SWF was loading.  I don’t have ServiceCapture on my work PC up here, so he had me install TamperData .  It’s a hot Firefox plugin that shows HTTP traffic.  You can see the SWF make a call for the Shared SWF when the asset is used; it’s pretty neat!  Great extension to have when you don’t have ServiceCapture.  Don’t know if it can see AMF though, only HTTP stuff I think.

Most of the Shared Library stuff is different in Flash Player 9 and Flex 2 .  I believe Roger mentioned awhile ago about how SimpleApplication uses like the Frame metadata to put shared font symbols on frame 3 or something… :: shrugs ::.  Anyway, getClassByName will allow access to other SWF’s libraries now so the Jenga world is a lot easier, but although the docs say you can’t do it in ActionScript only projects, I haven’t tested personally.  For those of us still doing Flash 8 and below, it works good.

Flashterbation

Heard this phrase used today to describe egregious usage of Flash. It was described also as:

Using Flash for the sake of using Flash

Funny. Apparently an outdated defintion exists on Wikipedia. Ah, how I’ve missed the design industry. I’ve gone from Irrelevant Implementation Details to Flashterbation. I reckon Flexterbation is when you use copious amounts of transitions because you can.