LegacyCaptivateLoader example updated

Just a note to point out that I updated the LegacyCaptivateLoader example to use SWFObject 2.1 (dynamic publishing). It has been successfully tested in the following systems:

  • Mac OS X:
    • Firefox 3.0.7
    • Opera 9.6.3
    • Safari 3.2.1
  • Windows XP:
    • Internet Explorer 6
    • Firefox 3.0.6
  • Windows Vista Business:
    • Internet Explorer 7
    • Firefox 3.0.2
    • Opera 9.5.2
    • Safari 3.2.1
  • Windows 7 Experimental
    • Internet Explorer 8 (Beta)

Note: the LegacyCaptivateLoader will probably fail if you embed the SWF using SWFObject 2’s static publishing option, as the nested elements will cause problems for the JavaScript.

Advertisements

SCORM files relocated… again.

Sorry to give everyone the runaround, but after trying Google Code for a month, I found it seriously lacking and not very fun to use. Therefore as of tonight, all of my SCORM files (and other goodies) are back at pipwerks.com on a dedicated downloads page.

LegacyCaptivateLoader: dealing with pre-existing scripts in your Captivate SWF

Many of us use a Flash-based course interface (a.k.a. ‘player’) to load Captivate SWFs and other content. A well-known stumbling block for this kind of ‘loaded SWF’ approach has been Captivate’s lack of ActionScript support — Captivate won’t allow a user to add a simple line of custom ActionScript anywhere. This means that Captivate does not natively support direct SWF-to-SWF communication.

Here’s a common scenario where this might be a problem:

A developer wants to load a Captivate SWF into a ‘player’ SWF. She wants the Captivate SWF to automatically unload when it’s done playing. To do this, she’d simply like the Captivate SWF to call an ActionScript function named “unloadMe()” at the end of the movie.

Since Captivate doesn’t support custom ActionScript, this seemingly innocent bit of scripting can’t be done… at least not natively.

The first band-aid

In my session on the topic at DevLearn 2007, I discussed two primary workarounds for enabling a Captivate SWF to communicate with the parent Flash-based SWF:

  1. Embedded SWFs: Import a Flash “ActionScript” SWF into the Captivate project. This embedded ActionScript contains a reference to a function stored in the Flash SWF’s main timeline; when the Captivate playhead hits the embedded SWF, it will invoke the ActionScript in the embedded SWF, which in turn invokes the function in the main Flash SWF.
  2. ExternalInterface: Use Captivate’s limited JavaScript support to make JavaScript calls, which are then handled by ExternalInterface code in the main Flash SWF.

From what I can tell, these workarounds have become a (begrudgingly) accepted common practice. The ’embedded SWF’ method in particular has been in use by the Captivate community for at least three years.

The second band-aid

Both workarounds work great when the ‘player’ SWF is using ActionScript 2.0 (the same version of ActionScript used by Captivate when publishing its SWFs), but with the release of ActionScript 3, a much larger stumbling block was introduced: ActionScript 2 SWFs cannot communicate with ActionScript 3 SWFs at all. By design! The simple explanation is that ActionScript 2 uses a different processing engine than ActionScript 3, and never the twain shall meet.

I recently posted a workaround on the topic: a Flash ActionScript 3 class (and proxy SWF) I named LegacyCaptivateLoader. It enables an ActionScript 3-based player to load and control Captivate SWFs via ExternalInterface and the proxy SWF.

When I designed the LegacyCaptivateLoader, I was focused on giving the ActionScript 3 SWF the ability to control the ActionScript 2-based Captivate SWF; I hadn’t given much thought to how the situation affects Captivate SWFs using one of the workarounds I just described. Can the embedded SWFs still work? Will JavaScript calls from Captivate still work with ActionScript 3’s ExternalInterface system? The short answer is yes, but it may take some tweaking on your part.

LegacyCaptivateLoader and the Captivate-to-Flash communication workarounds

Geez, that title sounds terrible, doesn’t it? It certainly doesn’t sound like fun. 🙂 Fear not, it isn’t as bad as it sounds. Let’s take a look at each of the workarounds and see how they fare with LegacyCaptivateLoader.

Workaround one: Embedding ActionScript SWFs in a Captivate SWF

I think it’s pretty obvious this one will need some attention. (And by the way, ActionScript 3 SWFs won’t work correctly when imported into Captivate, so don’t bother trying.)

Situation: Since Captivate uses ActionScript 2, any embedded SWF must also use ActionScript 2. That means that even if the embedded SWF refers to _root, now that the parent SWF is using ActionScript 3, the Captivate SWF and all of its contents can no longer communicate with the parent SWF.

Scenario: If we use the earlier example of a developer trying to call an ActionScript function named “unloadMe()” from Captivate, with the goal of invoking a function named unloadMe() in the main timeline of the parent SWF, we see that the AS2/AS3 barrier effectively kills the communication and renders our innocent little embedded SWF impotent.

Solution: Use the LegacyCaptivateLoader’s proxy SWF to listen for the embedded SWF’s calls and pass them to the AS3 SWF using ExternalInterface.

Yes, this definitely involves some custom coding on your part! It basically works like this:

The proxy SWF is an AS2 SWF that loads the Captivate SWF. That means the proxy SWF has become the defacto parent SWF; if the embedded SWF contains a call to _root.unloadMe(), Flash Player will look on the proxy SWF’s main timeline for a function named “unloadMe()”.

With me so far?

Here’s where the custom coding comes in: for every custom ActionScript call you expect to receive from the Captivate file, you’ll need to create a corresponding function in the proxy SWF and HTML file. The proxy SWF uses ExternalInterface to call the function in the HTML file, which in turn uses ExternalInterface to call the function in the AS3 file.

Complicated, yes. Fun, no. But it works.

Example A (using HTML)

Captivate SWF contains:

_root.unloadMe()

Proxy SWF contains:

function unloadMe(){
   ExternalInterface.call("unloadMe");
}

HTML contains:

function unloadMe(){
   var swf = document.getElementById("playerID");
   swf.unloadMe();
}

AS3 “Player” SWF contains:

var captivate:LegacyCaptivateLoader = new LegacyCaptivateLoader("playerID", "captivate.swf");

function unloadMe():void {
   captivate.unloadSWF();
}

ExternalInterface.addCallBack("unloadMe", unloadMe);

Note: you can also use the global ID method to skip the HTML function requirement, but it requires that you know the player SWF’s ID and hard-code it into the proxy SWF. This may not scale very well.

Example B (using global ID to avoid HTML)

Captivate SWF contains:

_root.unloadMe()

Proxy SWF contains:

function unloadMe(){
   ExternalInterface.call("playerID.unloadMe");
}

AS3 “Player” SWF contains:

var captivate:LegacyCaptivateLoader = new LegacyCaptivateLoader("playerID", "captivate.swf");

function unloadMe():void {
   captivate.unloadSWF();
}

ExternalInterface.addCallBack("unloadMe", unloadMe);

Workaround two: Using JavaScript in Captivate and ExternalInterface in the AS3 SWF

As far as I can tell, if you implemented JavaScript calls in your Captivate SWF, they should continue to work as before, without needing to modify the proxy SWF (yay!). Just be sure to update the functions contained in your HTML.

Example

Captivate SWF contains:

JavaScript: "unloadMe()"

HTML contains:

function unloadMe(){
   var swf = document.getElementById("playerID");
   swf.unloadMe();
}

AS3 “Player” SWF contains:

var captivate:LegacyCaptivateLoader = new LegacyCaptivateLoader("playerID", "captivate.swf");

function unloadMe():void {
   captivate.unloadSWF();
}

ExternalInterface.addCallBack("unloadMe", unloadMe);

What do you think?

While some (most?) of this is certainly a headache, my hope is that it will save you time compared to re-engineering and re-exporting all of your Captivate files. If you find this helpful, or if you know of a better, cleaner way to handle the situation (short of Captivate 4 coming along and being ActionScript 3 compatible), please let me know! 🙂

New: LegacyCaptivateLoader class

A few months ago I wrote about the dilemma of trying to load Captivate SWFs into an ActionScript 3-based parent SWF.

Dilemma solved!

I present to you the ActionScript 3 class LegacyCaptivateLoader. This class utilizes ExternalInterface and a proxy SWF to facilitate sending commands to and querying data from an ActionScript 2-based Captivate SWF.

How it works

The LegacyCaptivateController class extends sprite, and is basically a Loader with Captivate-specific methods. It uses ExternalInterface to facilitate synchronous communication between the AS3 SWF and the Captivate (AS2) SWF.

(Note: I previously created a LocalConnection workaround for AS3-to-Captivate communication, but the asynchronous nature of LocalConnection made it very difficult to work with, and ExternalInterface is more reliable.)

I will be the first to say this is not an elegant solution… it requires three hacks creative workarounds to enable the communication. However, it is easy to use, requires NO JavaScript in the HTML page, and — so far — seems to work fine in every browser I’ve tested (Windows IE6, FF2 and Safari 3.1 as of this writing, more testing will be done when I have the time).

The flow

  1. The AS3 SWF calls the class LegacyCaptivateLoader.
  2. The LegacyCaptivateLoader class automatically creates a Loader instance and loads a proxy SWF (AS2).
  3. The proxy SWF in turn contains a MovieClipLoader that loads the Captivate SWF.
  4. The queries and commands from AS3 go to the browser, where they are then routed to the proxy SWF via ExternalInterface. The proxy then sends commands directly to the Captivate SWF via direct ActionScript 2 communication.

Here hacky, hacky hacky!

As I mentioned, this system requires a few hacks. Here are the main ones:

Hack #1: The external proxy SWF. The AS3 SWF must load the proxy SWF for this system to work. The proxy SWF acts as a relay, which is necessary since we can’t add custom ActionScript ExternalInterface code directly to the Captivate SWF.

Hack #2: In order to avoid using JavaScript in the HMTL page or resorting to ugly eval() hacks in the ExternalInterface calls, we have to use the non-standard global namespace method of grabbing the SWF. So instead of using a DOM method such as

document.getElementById("myswf").myExternalInterfaceCall()

we simply use

myswf.myExternalInterfaceCall()

Standardistas cringe (me, too!), but in my mind it’s the least evil, most compatible, most secure and easiest to use hack of the bunch.

Hack #3: Because of hack #2, the HTML file must be in quirks mode (no DOCTYPE) to work in Firefox. Note that in my testing, the standards mode version (XHTML transitional) works in IE6 and Safari 3.1.

Give it a spin!

Please give the class a try and let me know how it works for you… success stories and bugs! (Please add comments to this thread or post at the eLearning Technology and Development Google Group). Thanks!

Loading Captivate files into an AS3 Flash SWF

Update April 7, 2008: I’ve written a new AS3 class named LegacyCaptivateLoader that uses ExternalInterface to bridge the AS3 SWF and the Captivate SWF. Check it out.

I guess I’m late to the party, but I only recently realized that although a Flash Player 9 SWF can load an older Flash Player 6/7/8 SWF, it can’t communicate with it.

(In my defense, since we haven’t really started using ActionScript 3 at work yet, I’ve been a bit slow in switching to AS3. The leap from AS2 to AS3 is pretty daunting, so I’m sure I’m not the only one dragging my feet!)

Turns out the mechanism that processes the ActionScript (the ActionScript Virtual Machine, or “AVM” for short) has been rebuilt. Flash Player now ships with two unique ActionScript processors: AVM1 for legacy ActionScript 1 & 2 SWFs, and AVM2 for ActionScript 3 SWFs. Without getting overly technical, this enables AVM2 (Flash Player 9 SWFs) to be exponentially faster than AVM1 (Flash Player 8 and lower SWFs).

As you can imagine, many Flash developers — like you and me — still need to load old SWFs into a new Flash Player 9 (ActionScript 3) user interface. For instance, many Flash-based e-learning courses load ‘content’ SWFs that were created a couple of years ago with Flash MX (7) or Captivate. No one wants to recreate or republish a few years’ worth of development files.

To accommodate people who still need to use their older SWFs, Adobe configured Flash Player to allow AMV2 SWFs to load the older AVM1 SWFs in a virtual sandbox. But, as I mentioned, there’s a catch: these AVM1 SWFs cannot communicate with the parent AVM2 SWF.

This is a very big problem for many Adobe Captivate users. Adobe didn’t wait for the Captivate development team to convert Captivate to AS3, which means Captivate users are still publishing AS2 SWFs on a daily basis. A large number of Captivate users ‘play’ their Captivate files in custom Flash-based course interfaces. If they want to use a newer AS3 interface to control their Captivate SWFs (play, pause, etc.), they’re out of luck.

I guess you can’t blame Adobe for not updating Captivate’s codebase; Captivate has probably had the same codebase since early versions of RoboDemo, and converting to AS3 would probably require a complete overhaul of the product. No small task.

The Experiment

Anyway, I’ll get to the point: I researched the different methods available for AVM1 to AVM2 communication, and discovered there are a few workarounds that can enable the AS3 SWF to communicate with the AS2 SWF. I spent the entire day whipping up a Captivate-specific proof-of-concept, which can be viewed here.

For this experiment, I used LocalConnection. I’ve also been researching an ExternalInterface method, but the LocalConnection method was much easier to implement and doesn’t require JavaScript.

Because LocalConnection requires the old SWF to have specific LocalConnection code inside it, we can’t use LocalConnection on Captivate SWFs without a little help. I was able to use a proxy SWF to load the Captivate movie.

I’m not ready to explain the code and hand out the source files, but I hope this proof of concept can help others out. The short version is:

  1. A ‘player’ SWF (AS3) loads an AS2-based ‘proxy’ SWF. This proxy SWF is configured with custom LocalConnection settings allowing it to send and receive commands from the AS3 player.
  2. The proxy SWF loads the Captivate SWF. Since the proxy SWF and Captivate SWF are both AS2, they can communicate with each other using the famed ‘Captivate variables’.

Thus the AS3 SWF sends instructions to the proxy SWF, which relays the instructions to the Captivate SWF. Conversely, the Captivate SWF sends data (frame count, current slide, etc.) to the proxy SWF, which then sends the data via LocalConnection to the AS3 SWF.

BTW, using LocalConnection to bridge AVM1 and AVM2 isn’t an original idea… many people have blogged about these concepts over the last year or two, and had some good tips (see my references at the end of this post). There are even a few functional commercial and freeware products out there.

I decided to develop my own method out of curiosity, and because most of the existing products are overly complicated, designed to handle way more than my dinky little Captivate files. Plus I wanted to create a system that would have the ‘Captivate variables’ built-in, so it will be plug-and-play with any Flash-based Captivate loader.

Caveats

There are some very big caveats when using LocalConnection to bridge AVM1 and AVM2 SWFs; these caveats are big enough to make me question just how far I want to go with this project.

Caveat #1: LocalConnection is asynchronous. This means it can’t return values, and it may not kick in as soon as you’d hoped. I learned firsthand that LocalConnection worked much faster in my local environment than it did after I uploaded it to the server.

Caveat #2: LocalConnection works independent of the browser, and can only have ONE active connection per unique LocalConnection session. For instance, if I create a course that uses a LocalConnection named “FlashToCaptivate_LC”, I can only have one instance of that course running on my computer. If I open a second instance of the course, regardless of which browser it’s in, or whether or not the course is local or online, the second course will return a LocalConnection error because the connection named “FlashToCaptivate_LC” is already in use. Think of it as a phone number without call waiting. If someone is on the phone and you try calling, all you’ll get is a busy signal. That’s LocalConnection.

What do you think?

I’d love to hear any feedback you might have about this topic, including whether or not any of you have tried LocalConnection yourselves.

Resources

Here are some good resources/discussions about the topic if you’d like to learn more (no particular order):