Tuesday, January 12, 2010

ActionScript 2.0 to JavaScript Bridge

As discussed in my new year Ajaxian post, the Flash Player could provide some truly good info about the current user.
I have tried before to create a sort of "perfect bridge" to actually drive ActionScript directly via JavaScript.
Unfortunately, the layer added by the ExternalInterface is not light at all and things would move so slowly that it won't be worth it.
While in my post comments I have showed an example about how it is possible to export info from ActionScript to JavaScript, what I am describing right now is a sort of "general purpose bridge" to bring, statically and stateless, whatever info we need/want from whatever ActionScript 2.0 file/library we like, starting from the root.

Basic Example



<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>AS2Bridge by WebReflection</title>
<script type="text/javascript" src="AS2Bridge.js"></script>
</head>
<body>
<script type="text/javascript">

// override defaults showing the SWF
new AS2Bridge({width:320, height:200, style:""}).onload = function(){
var System = this.retrieve("System");
window.console && console.log(System);
setTimeout(function () {
System.showSettings();
}, 1000);
};

// another movie, an hidden one, just to retrieve some global AS 2.0 property
new AS2Bridge().onload = function(){
var _quality = this.retrieve("_quality");
window.console && console.log(_quality);
};

</script>
</body>
</html>

The AS2Bridge constructor creates each time, and if necessary, an instance related to the current ActionScript 2.0 global scope.

Single Method API: retrieve

Each instance will have a couple of public static properties, as is for example its related object id, plus a single public method:
self.retrieve(namespace:String):Object

Above method accepts a generic namespace string, which is basically the whole path, starting from the root, that will be exported.
This string can point to a property, a generic object, or even a function or method ... does it sound interesting?
As example, it is possible to export the whole System public static object:

var System = this.retrieve("System");

or just one part of it:

var playMusic = this.retrieve("System.capabilities.hasAudio");


The ActionScript 2.0 Code


(function ():Void {
/*! (C) Andrea Giammarchi - Mit Style License */

/** dependencies */
import flash.external.ExternalInterface;

/**
* executes a function and returns its exported value
* @param Number the registerd callback to exec
* @param Array arguments to send
* @return String exported result via callback
*/
function exec(i:Number, arguments:Array):String {
return callback[i].apply(null, arguments);
};

/**
* evaluates a namespace and exports it
* @param String a generic namespace to retrieve
* @return String exported namespace or one of its properties/methods
*/
function retrieve(key:String):String {
return export(eval(key), self);
};

/**
* Exports a generic object injecting its parent scope.
* @param Object generic property, value, method, or namespace to export
* @param Object parent to inject if a method is encountered ( e.g. (o = showSettings).call((p = System)) )
* @return String exported Object, the first argument, JavaScript compatible
*/
function export(o:Object, p:Object):String {
var type:String = typeof o;
switch (true) {
case o === null:
case type === "number":
case o instanceof Number:
if (isNaN(o)) {
return "null";
};
case type === "boolean":
case o instanceof Boolean:
return "" + o;
case type === "function":
case o instanceof Function:
return "function(){return " + bridge + ".__eval__('return '+" + bridge + ".__swf__.AS2Bridge__exec__(" + (callback.push(
function ():String {
return export(o.apply(p, arguments), p);
}
) - 1) + ",Array.prototype.slice.call(arguments)))()}";
case type === "string":
case o instanceof String:
return 'decodeURIComponent("' + escape("" + o) + '")';
case o instanceof Array:
for (var a:Array = new Array(), i:Number = 0, length:Number = o.length; i < length; ++i) {
a[i] = export(o[i], a);
};
return "[" + a.join(",") + "]";
case o instanceof Date:
return "new Date(" + o.getTime() + ")";
case o instanceof Object:
var a:Array = new Array();
var i:Number = 0;
for (var key:String in o) {
a[i++] = key + ":" + export(o[key], o);
};
return "{" + a.join(",") + "}";
default:
return "undefined";
};
};

/**
* Stack of stored callbacks encountered during exports.
* @private
*/
var callback:Array = new Array();

/**
* _root alias
* @private
*/
var self:Object = this;

/**
* String shortcut to use to point to the current bridge object (passed via query string or FlashVar)
* @private
*/
var bridge:String = "AS2Bridge.__register__[" + self.AS2Bridge__index__ + "]";

/**
* unobtrusive exposed callbacks
*/
ExternalInterface.addCallback("AS2Bridge__exec__", this, exec);
ExternalInterface.addCallback("AS2Bridge__retrieve__", this, retrieve);

/**
* JavaScript initialization
*/
ExternalInterface.call("eval", bridge + ".__init__&&" + bridge + ".__init__()");

}).call(this);

// optional stop to avoid loops if/when necessary
stop();

Thanks to a single closure, this snippet is desgned to work as stand alone, or as "general purpose no conflict plugin". In few words, whatever happens in the SWF it is always possible to retrieve a namespace/property/method and it is always possible from the SWF to call JavaScript:

// it is possible to call any public function without problems, e.g.
ExternalInterface.call("eval", 'alert("Hello from SWF")');


The ASBridge Constructor


if (typeof AS2Bridge !== "function") {
/*! (C) Andrea Giammarchi - Mit Style License */
var AS2Bridge = function (options) {
var index = AS2Bridge.__register__.push(this) - 1;
if (options) {
for (var key in options) {
this[key] = options[key];
};
};
this.id += index;
this.src += "?AS2Bridge__index__=" + index;
document.write(document.body ? this.__swf__() : "<body>" + this.__swf__() + "</body>");
};
AS2Bridge.__register__ = [];
AS2Bridge.prototype = {
constructor: AS2Bridge,
id: "__AS2Bridge__",
src: "AS2Bridge.swf",
style: "position:absolute;top:-10000px;left:-10000px;",
width: 1,
height: 1,
retrieve:function (key) {
return this.__eval__("return " + this.__swf__.AS2Bridge__retrieve__(key))();
},
__eval__:Function,
__init__:function(){
this.__swf__ = window[this.id] || document[this.id];
this.__init__ = null;
if (this.onload) {
this.onload();
};
},
__swf__:function () {
return ''.concat(
'<object type="application/x-shockwave-flash" ',
'style="', this.style, '" ',
'id="', this.id, '" ',
'width="', this.width, '" ',
'height="', this.height, '" ',
'data="', this.src, '">',
'<param name="movie" value="', this.src, '" />',
'</object>'
);
}
};
};

I am sorry I had no time to put proper comments, but I think it is simple enough to understand what's going on there.
There are defaults options, plus an horrible document write which aim is simply the one to put the SWF in place avoiding body problems.
Anyway, AS2Bridge would like to be after the head element, possibly inside the body ... the important thing is that it works, so why bother? ;)

The Whole Demo

With 908 bytes of cacheable SWF, and a ridiculous sice for the minified and gzipped JavaScript file, it is possible to test all I have said directly here.

Integration

ActionScript 3 a part, and would be nice to have time to create a bridge for it as well, it is possible to copy and paste in the root of whatever project my AS2.0 code in order to bring ActionScript to JavaScript export capability everywhere.
... told'ya it was something interesting, or maybe not ...

No comments:

Post a Comment