Captivate and SCORM. Not the greatest buddy movie you’ve seen, and certainly not as awesome as peanut butter and chocolate.
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
- 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:
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?
ExternalInterface doesn’t require VBScript.
Question: What’s it doing here?
Answer: Absolutely nothing!
Captivate_DoExternalInterface functions are still being used but it appears the
Captivate_DoFSCommand function is not really functional, so we can delete it.
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
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
Captivate_DoExternalInterface function still has a number of issues. The first is useless variable reassignment. In the following example,
strFSArg3 are all useless reassignments.
This reassignment has no benefit and makes the code less readable, so we’ll clear it out, using use
Also, every time
Captivate_DoExternalInterface is invoked, it performs a lookup to find the Captivate SWF in the page’s DOM:
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.
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
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.
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
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.
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.
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
Cleanup task #7: Prevent redundant
Captivate is horrible about sending the same calls to the server over and over again. For example, Captivate sets
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.
Which is used like this:
I tested this code with a simple quiz-style course; the course contains 8 questions. I logged every call to
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
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
GetValue call, for a total of 273 times. Ideally,
GetLastError would only be invoked if the
GetValue attempt failed.
SetValue returns a boolean in string form (
"false") indicating the success of the call. We can prevent many of the
GetLastError invocations by limiting them to situations where
"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
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 invocations
GetValue= 12 invocations
Commit= 19 invocations
GetLastError= 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:
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.