In part one of this series, we published a simple Captivate course and examined its file structure. In part two, we cleaned up the HTML file and externalized all JavaScript. In part three, we cleaned up the JavaScript. In this installment, we will examine and update the SCORM code.
Captivate and SCORM. Not the greatest buddy movie you’ve seen, and certainly not as awesome as peanut butter and chocolate.
Captivate’s SCORM code — both the JavaScript you’ve seen in the publishing templates and the ActionScript you can’t see inside the SWF — were written eons ago, when Internet Explorer 6 was the latest and greatest browser, and Firefox was still a twinkle in Mozilla’s eye.
Communication between a Flash SWF and JavaScript was an evolving issue; ExternalInterface hadn’t been created yet, so developers had to choose between the synchronous FS Command, which only worked in Internet Explorer for Windows (no Netscape or IE for Mac), or the asynchronous getURL hack, which worked across browsers but required clumsy timeouts and polling. It also caused a slew of clicking sounds every time it was used.
In 2005, Mike Chambers and his cohorts at Macromedia created the ExternalInterface API for ActionScript, which provided Flash developers direct synchronous access to JavaScript, and vice-versa. Since ExternalInterface works cross-browser and cross-platform, it was (and still is) a godsend for e-learning developers.
ExternalInterface shipped with Flash Player 8 in 2005.
RoboDemo — Captivate’s product name before it was rechristened in 2004 — supported SCORM 1.2. From what I’ve read, most of the SCORM integration was performed by Andrew Chemey. Since ExternalInterface hadn’t been released yet, Andrew implemented both the FSCommand and getURL approaches, enabling RoboDemo’s SCORM-based courses to work in all browsers. It was a fine bit of work for its day.
Taking a look at the Captivate product release timeline, the first edition of Captivate was released in 2004. This is when SCORM 2004 support was introduced to Captivate, as an add-on to the existing SCORM 1.2 support.
The SCORM support code has barely been touched since then. Here’s a simplified look at Captivate’s lifespan with regards to SCORM:
- 2005: Adobe acquires Macromedia.
- 2006: Adobe Captivate 2 released, with no changes to the SCORM support code.
- 2007: Adobe Captivate 3 released, boasting “Improved learning management system (LMS) integration” but with no substantial changes to the SCORM publishing template.
- 2008: radio silence.
- 2009: Adobe Captivate 4 released, introducing the option to publish to ActionScript 3. ExternalInterface is introduced under-the-hood, but is not used universally. The SCORM code still uses the old FSCommand and getURL techniques, depending on browser (hence all the blog and forum posts about the
g_intAPIType
hack). - 2010: Adobe Captivate 5 released. Touted as being “built from scratch,” the SCORM publishing templates still contain original SCORM support code from Macromedia Captivate, and still uses LocalConnection/getURL to communicate in Mozilla-based browsers, most notably Mozilla Firefox.
- 2011: Adobe Captivate 5.5 released, with one big change: ExternalInterface has become the exclusive communication method for SCORM. FSCommand and getURL are never used… but the templates aren’t updated to reflect this change!
Captivate 5.5 finally allows us to breathe a little sigh of relief; as of 2011 — a full six years after ExternalInterface was released — Adobe finally made ExternalInterface the sole technique for Captivate’s SCORM communication. This greatly simplifies the codebase and browser support.
But hang on — why does the SCORM 2004 publishing template still contain all that legacy code? What a drag, Adobe shipped outdated legacy code in a paid update. Again.
Snarkiness aside, it’s interesting to note that Adobe hasn’t really publicized the transition from the old communication techniques to ExternalInterface. I challenge you to find any official documentation on the topic! However, if you look very closely at the unedited publishing template, you’ll see one clear indication of the ExternalInterface update in Captivate 5.5:
var g_intAPIType = 0;
// Hook for Internet Explorer
if ((navigator.appName && navigator.appName.indexOf("Microsoft") != -1 && navigator.userAgent.indexOf("Windows") != -1 && navigator.userAgent.indexOf("Windows 3.1") == -1) || g_intAPIType == -1)
{
g_intAPIType = 0;
// ... other stuff ...
} else {
g_intAPIType = 0;
}
Code language: JavaScript (javascript)
Regardless of the outcome of the browser detection in the ‘if’ clause, g_intAPIType
will be set to 0. This is a change; in prior versions of Captivate, the ‘else’ clause would set g_intAPIType
to 1, forcing non-Internet Explorer browsers to use the getURL approach.
This code excerpt is also a great example of poor quality control on Adobe’s part; the variable g_intAPIType
is declared and given a value of 0 just before the if()
block. So why does the conditional code include a check for g_intAPIType == -1
? Any why even include g_intAPIType
in the if/else statement anyway, if you know you aren’t going to change the value?
Regardless, we’ve clearly established the template needs cleaning, so let’s roll up our sleeves and get to work!
Cleanup task #1: Remove non-External Interface code
Since we know that “setting g_intAPIType
to 1 means you’re forcing Captivate to use getURL in all browsers”, and getURL is no longer an option in Captivate 5.5, we can remove any code that uses g_intAPIType
with a value of anything other than 0.
We also know that Captivate will never use any of the FSCommand code, so we can remove that, too.
This leaves us with a strange little chunk of code… remember the VBScript I mentioned in a previous post?
var g_intAPIType = 0;
// Hook for Internet Explorer
if ((navigator.appName && navigator.appName.indexOf("Microsoft") != -1 && navigator.userAgent.indexOf("Windows") != -1 && navigator.userAgent.indexOf("Windows 3.1") == -1) || g_intAPIType == -1)
{
g_intAPIType = 0;
document.write('<script language=\"VBScript\"\>\n');
document.write('On Error Resume Next\n');
document.write('Sub Captivate_FSCommand(ByVal command, ByVal args)\n');
document.write('Call Captivate_DoFSCommand(command, args)\n');
document.write('End Sub\n');
document.write('</ script\>\n');
document.write('<script language=\"VBScript\"\>\n');
document.write('On Error Resume Next\n');
document.write('Sub Captivate_DoExternalInterface(ByVal command, ByVal parameter, ByVal value, ByVal variable)\n');
document.write('Call Captivate_DoExternalInterface(command, parameter, value, variable)\n');
document.write('End Sub\n');
document.write('</ script\>\n');
} else {
g_intAPIType = 0;
}
Code language: JavaScript (javascript)
ExternalInterface doesn’t require VBScript.
Question: What’s it doing here?
Answer: Absolutely nothing!
[Gong!] Delete!
Believe it or not, we have just removed the last bit of browser sniffing from our JavaScript code. Our remaining code is browser-agnostic and no longer contains special handling for Internet Explorer. Yay!
The Captivate_DoFSCommand
and Captivate_DoExternalInterface
functions are still being used but it appears the Captivate_DoFSCommand
function is not really functional, so we can delete it.
(I removed Captivate_DoFSCommand
to see if it caused any problems; the course worked in both Internet Explorer and Chrome without issues, so appears safe to chuck it.)
If ExternalInterface is truly the exclusive communication technique, we can also remove the dataToFlash
and dataFromFlash
functions, since these were only used by the old getURL communication technique.
We can also remove the “NS_dynamic” layer element from the HTML file.
The remaining SCORM code is looking much, much leaner and meaner! We still have lots of optimizations we can make, though.
Cleanup task #2: Optimize Captivate_DoExternalInterface
and cache the reference to the Captivate SWF
The Captivate_DoExternalInterface
function still has a number of issues. The first is useless variable reassignment. In the following example, strFSCmd
, strFSArg1
, strFSArg2
, and strFSArg3
are all useless reassignments.
function Captivate_DoExternalInterface(command, parameter, value, variable){
var CaptivateObj = document.getElementById("Captivate"),
strFSCmd = new String(command),
strErr = "true",
strFSArg1 = parameter,
strFSArg2 = value,
strFSArg3 = variable,
courseStatus;
Code language: JavaScript (javascript)
This reassignment has no benefit and makes the code less readable, so we’ll clear it out, using use command
, parameter
, value
, and variable
directly.
Also, every time Captivate_DoExternalInterface
is invoked, it performs a lookup to find the Captivate SWF in the page’s DOM:
var CaptivateObj = document.getElementById("Captivate")
Code language: JavaScript (javascript)
This is wasteful and a surefire way to worsen your course’s performance. It’s best to create a global reference to the Captivate SWF and re-use it, rather than looking up the SWF every time. We can do this quickly and easily with a slight modification to the callback function we created for our SWFObject embed script.
var CaptivateSWF;
function callbackFn(e){
//e.ref is the <object> aka SWF file. No need for getElementById
if(e.success && e.ref){
CaptivateSWF = e.ref;
CaptivateSWF.tabIndex = -1; //Set tabIndex to enable focus on non-form elements
CaptivateSWF.focus();
}
}
Code language: JavaScript (javascript)
Last but not least, there are a couple of places where we can simplify the syntax, and as a best practice, we should be using a strict comparison operator ===
instead of the loose comparison operator ==
.
Cleanup task #3: Get rid of the timer
There’s a timing issue with our template: the template’s JavaScript code is trying ensure the SCORM API is found before embedding the SWF, but it’s doing so with a timer, which is clumsy and can slow down load time. It’s better to let the getAPI
function run its course first, then do a simple conditional check to see if the SCORM API has been found. If yes, embed the SWF. If no, show a message informing the learner to try again or contact the course administrator.
While we’re at it, we can clean up the SWF’s URL. In the original template, the variable strURLParams
is used to add two querystring parameters to the SWF’s URL; these parameters indicate the version of SCORM being used, and the communication technique being used.
if(g_objAPI != null)
{
strURLParams = "?SCORM_API=" + g_zAPIVersion + "&SCORM_TYPE=" + g_intAPIType;
}
var so = new SWFObject(strURLFile + strURLParams, "Captivate", "641", "512", "10", "#CCCCCC");
Code language: JavaScript (javascript)
Since we know we’re using SCORM 2004 with ExternalInterface, we can hardcode the value of strURLParams
to the SWF’s URL. This enables us to delete that last remaining references to strURLParams
and g_zAPIVersion
.
swfobject.embedSWF(strURLFile + "?SCORM_API=1.0&SCORM_TYPE=0" ...
Code language: JavaScript (javascript)
Cleanup task #4: Clean up the ‘find’ code for the SCORM API
Even though this is a SCORM 2004 template, there’s still a bit of waffling in the code — the code includes sniffing for the SCORM 1.2 API. We should remove it.
Frankly, I’ve never been a fan of Captivate’s findAPI
approach, so I’m going to replace it with the findAPI
approach used in my pipwerks SCORM wrapper. It follows the approach of ADL and Rustici Software (scorm.com), is thoroughly tested, and IMO is much easier to troubleshoot.
Cleanup task #5: Improve the exit/finish handling
Proper use of an unload handler is very important for SCORM courses. It ensures the course exits properly and saves the learner’s data.
Captivate’s publishing template includes some unload handling, but we can improve it. For example, we can ensure cmi.exit
is set to a reasonable default, and we can add an onbeforeunload
handler, which is helpful in Internet Explorer. These steps will help ensure the course can be resumed if the learner leaves before completing it.
var unloadHandler = function (){
if(!unloaded && isInitialized && !isTerminated){
var exit_status = (courseStatus === "incomplete") ? "suspend" : "normal";
SCORM_API.SetValue("cmi.exit", exit_status); //Set exit to whatever is needed
SCORM_API.Commit(""); //Ensure that LMS saves all data
isTerminated = (SCORM_API.Terminate("") === "true"); //close the SCORM API connection properly
unloaded = true; //Ensure we don't invoke unloadHandler more than once.
}
};
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;
Code language: JavaScript (javascript)
Cleanup task #6: Initialize SCORM connection earlier
There are a number of tweaks we can make to improve Captivate’s SCORM handling. For example, Captivate finds the SCORM API before embedding the SWF, but then sits on its thumbs; it doesn’t initialize the connection until well after the SWF loads.
Personally, I think it’s better to have JavaScript get SCORM’s motor running before the SWF has finished loading, as it will reduce course startup time. The problem is, if we initialize the SCORM connection ourselves, the Initialize()
command sent by the SWF will cause an error — SCORM doesn’t allow initializing twice! The solution is to neuter the Initialize()
handling in Captivate_DoExternalInterface
.
...
if(command === "Initialize"){
//We already initialized, just nod politely
//and tell the SWF everything is okay!
} else if(command === "SetValue"){
...
Code language: JavaScript (javascript)
Cleanup task #7: Prevent redundant SetValue
calls
Captivate is horrible about sending the same calls to the server over and over again. For example, Captivate sets cmi.score.max
and cmi.score.min
on every slide! In well-designed courseware, these are typically values that get set once at the beginning of a course and are untouched thereafter. Let’s ensure Captivate only sends the value once; if it tries to send the same value a second time, let’s intervene and prevent it from being sent to the LMS.
We can also prevent Captivate from sending other values that haven’t changed since the last time they were sent. For example, if the completion_status
is already “incomplete”, we can prevent the course from sending the “incomplete” value again (which Captivate does over and over and over). This helps shield the LMS from a deluge of useless calls, lightening its load and hopefully improving its performance.
I’ve added a cmiCache
function that stores values sent to the LMS; if the Captivate SWF tries to send a value that has already been sent, cmiCache
will prevent it from going to the LMS.
var value_store = [];
var cmiCache = function(property, value){
//Ensure we have a valid property to work with
if(typeof property === "undefined"){ return false; }
//Replace all periods in CMI property names so we don't run into JS errors
property = property.replace(/\./g,'_');
//If cached value exists, return it
if(typeof value_store[property] !== "undefined"){
return value_store[property];
}
//Otherwise add to cache
if(typeof value !== "undefined"){
value_store[property] = value;
}
return false;
};
Code language: JavaScript (javascript)
Which is used like this:
//Check to see if value is already cached
var cached_value = cmiCache(parameter, value);
//Only send value to LMS if it hasn't already been sent;
//If value is cached and matches what is about to be sent
//to the LMS, prevent value from being sent a second time.
if(!cached_value || cached_value !== value){
//Not cached. Sending to LMS.
} else {
//param and value has already been sent.
//Preventing redundant LMS communication.
}
Code language: JavaScript (javascript)
I tested this code with a simple quiz-style course; the course contains 8 questions. I logged every call to Captivate_DoExternalInterface
. SetValue
was invoked by the SWF 261 times; the new cmiCache
feature successfully prevented 70 redundant SetValue
calls (a 26% reduction), reducing the total LMS load to 191 SetValue
calls. Still a lot, but an improvement.
Update: The cache handling has been refactored to fix a bug and simplify its use. See the modifications here.
Cleanup task #8: Prevent redundant GetLastError
calls
Looking at the log from task #7 is quite revealing. For example, Commit
(save) was invoked 19 times — always after setting suspend_data — and GetLastError
was invoked after each and every SetValue
and GetValue
call, for a total of 273 times. Ideally, GetLastError
would only be invoked if the SetValue
or GetValue
attempt failed.
SetValue
returns a boolean in string form ("true"
, "false"
) indicating the success of the call. We can prevent many of the GetLastError
invocations by limiting them to situations where SetValue
returns "false"
. We won’t bother monitoring GetValue
calls, because they’re trickier to work with (don’t return booleans indicating success) and they aren’t used very often in Captivate. The log from task #7 shows 12 GetValue
calls compared to 261 SetValue
calls.
While we’re at it, let’s remove the redundant CaptivateSWF.SetScormVariable(variable, strErr);
calls… we like our code nice and DRY.
After launching the course again and looking at the log, we see the following data:
SetValue
= 261 invocationsGetValue
= 12 invocationsCommit
= 19 invocationsGetLastError
= 273 invocations
Yes, you read that correctly — Captivate is trying to hit the LMS 565 times for an 8 question quiz!
The log also reveals the result of the prevention script:
- 258
GetLastError
preventions - 74
SetValue
preventions
332 LMS hits were prevented. That reduces the course’s load on the LMS by over 50%! (Down from 565 to 233.)
If I had a nickel for every time I prevented Captivate from calling the LMS in this course, I’d have $16.60, enough to buy a nice lunch.
That’s about it for the SCORM edits. We can nitpick some more, but the basic functionality is set, and most of the advanced functionality is handled internally by the SWF.
In the next installment of this series, I’ll go over file organization and discuss how to use these edited files in your Captivate publishing templates folder. Go to Part 5.