Update to SCORM Wrapper

Made a minor update: scorm.quit() was setting a value (cmi.exit) but not invoking scorm.save() prior to termination. This could lead to failure to persist the value of cmi.exit in the LMS.

View the latest update on GitHub

Advertisements

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 4: SCORM

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;
}

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;
}

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;

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")

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();
    }
}

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 ==.

View the result of task #2

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");

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" ...

View the result of task #3

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.

View the result of task #4

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;

View the result of task #5

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"){
...

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;

};

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.
}

View the result of task #7

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.

View the result of task #8

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:

  • 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.

SCORM “Planets” Example Updated

Update 10/13/2011: I added more comments to the ActionScript code to help explain what each line of code does. I also added a Flash CS4 version of the FLA to the ZIP so more people can access the example.

My “Planets” example (How to Add Basic SCORM Code to a Flash Movie) has proven to be one of the most popular items on pipwerks.com. Unfortunately, it was designed as a quick example and had a bunch of flaws and shortcomings. It’s also about 3 years old and starting to show its age. Since people frequently contact me with questions — many of which were due to the flaws in the example — I decided to update the project.

The new Planets example has a bunch of fixes:

  • Full compliance with SCORM XSD requirements (it includes all those pesky XSD files that really don’t do anything but are required anyway).
  • Upgraded the pipwerks JavaScript wrapper to the latest version
  • Upgraded the pipwerks ActionScript 3 wrapper to the latest version
  • Upgraded SWFObject from 1.5 to 2.2
  • Changed the doctype and markup to HTML5
  • Added an unload handler to the JavaScript to ensure course progress is saved if the learner exits early
  • Added support for tracking progress across attempts using cmi.suspend_data
  • Completely rebuilt the FLA file to remove all timeline-based animation and scripts. Now uses a single ActionScript file, loaded on a single frame. All animations (fade transitions) are handled via ActionScript. This makes the file smaller and hopefully less confusing.
  • The SCORM code is clearly separated from the rest of the movie’s ActionScript; it’s in the same file, but not intermingled with other functions.

I haven’t decided if I’ll create a detailed tutorial to explain the updates; I’ve added a ton of comments to the ActionScript file, and hope it’s clear enough for others to follow.

Download the updated Planets example
The ZIP includes two FLA files: one in CS4 format, and the other in CS5.5. The code, movieclips, and graphic elements inside the FLAs are exactly the same.

Abstracting Your Course’s Tracking Code

An abstraction layer is a way of hiding complexities and maintaining cleanliness in your application. For example, if you want to save a file in your word processing application, you simply click “save”, while a whole host of actions is performed for you under-the-hood. In this example, you’re shielded from the complexity of your computer’s I/O operations by a graphical user interface.

When integrating tracking support (SCORM, AICC, etc,) into an an e-learning course, it’s a good idea to abstract as much of the tracking code as possible. For example, if you know your course is going to use SCORM 1.2, you might be tempted to write this:


//Assuming API is the SCORM API

//Display learner's name
var learner_name = API.LMSGetValue("cmi.core.student_name");

//Set course status to incomplete
API.LMSSetValue("cmi.core.lesson_status", "incomplete");

//Tell LMS to save whatever data we've sent
API.LMSCommit("");

This example hard-codes SCORM 1.2 into your course. If you ever decide to use SCORM 2004, you’d need to re-write whole chunks of your course. If you abstract the code by creating functions, you’re making it a little easier to update your code in the future:


//Assuming API is the SCORM API

function getLearnerName(){
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   API.LMSSetValue("cmi.core.lesson_status", "incomplete");
}

function save(){
   API.LMSCommit("");
}

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

Now that your tracking code is encapsulated in functions, it can be easily modified to include SCORM 2004 support:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

For e-learning courses, abstraction becomes even more powerful if you externalize your tracking-related functions. For example, you’d keep your tracking function invocations in your course code while placing the function definitions in a separate, easy-to-access file. This improves the separation of the tracking mechanism (SCORM, AICC, etc.) from the actual course logic.

Your course code:


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

An external JavaScript file containing the function definitions:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Looking good, except for one thing… do you spot the big problem with this example? The tracking_mode variable is hard-coded in the course. A quick resolution is to dynamically set this variable via a querystring parameter when you load the course:


//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

At this point, your course code is abstracted enough that it would be (relatively) trivial to add AICC support by modifying your tracking functions.

As you can see, the further we abstract, the cleaner your course’s code becomes. If you’re a Flash developer, use an additional level of abstraction — place your tracking functions in JavaScript and invoke them via ExternalInterface. While it may add a little up-front work, it will enable you to easily update your tracking code without needing to republish your FLAs.

Your course’s JavaScript:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Your course’s FLA:


//Display learner's name
var learner_name:String = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

And here’s the glue: an abstraction layer meant to bridge the course FLA and the JavaScript tracking. Save as an external AS file (or a class/package if you’re feeling adventurous).


import flash.external.*;

function getLearnerName():String {
   return ExternalInterface.call("getLearnerName");
}

function setLessonIncomplete():void {
   ExternalInterface.call("setLessonIncomplete");
}

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

In this example the tracking mode is set via JavaScript, and all of the tracking functionality is handled via JavaScript. If you ever needed to change from SCORM 1.2 to SCORM 2004 or AICC, you could handle it all via your JavaScript file and leave your Flash untouched. Easy breezy.

These are very simple examples, but hopefully they show the power of abstraction. Notice the last two examples have zero mention of SCORM in the course code itself. Sweet!

Of course, there is a downside to everything. As I mentioned in a thread on the E-Learning Technology and Development Google Group, abstracting like this takes time and can add quite a bit of work to your course development plan. If you know your course is a one-off that will only ever use one tracking method (such as SCORM 1.2), abstraction layers may be more effort than they’re worth. But if you’re not in a hurry, or know that your course will be around for a long time and will run on multiple LMSs, abstraction is a very good idea.

Abstraction and the pipwerks SCORM API Wrappers

What about the pipwerks SCORM API Wrappers? Well, they use some abstraction, but are not as abstracted as these examples. They can certainly help you on your way, but would need a bit of tweaking to reach this level of abstraction. I’m currently working on new SCORM wrappers that aim to support this level of abstraction. Stay tuned.

pipwerks SCORM Wrappers now available on GitHub

I’ve been considering adding my pipwerks SCORM wrappers to GitHub for a very long time, but my n00bness and general lack of free time were major obstacles. However, the time has finally come to buckle down and get these puppies OUT! So without further ado, I present:

https://github.com/pipwerks/scorm-api-wrapper

I should note that these are my original SCORM wrappers (JavaScript, ActionScript 2 and ActionScript 3); I’ve been (very slowly) working on a completely new SCORM support codebase that I plan to release separately. However, these oldies are still running like a champ, so don’t be afraid to use them.

Many people have contacted me over the last few years with suggestions for improvements, but due to my general busy-ness I haven’t really made any modifications for a long time; I recently made a few small tweaks, including updating the StringToBoolean function that caused problems in Plateau, but the bulk of the code remains largely the same.

If you’re one of those die-hards eager to tweak the source code, you’re in luck! This is a public repository, which means anyone and everyone is now free to fork and edit the code as they see fit. If you have any suggestions or ideas, please go to GitHub and show us what ya got!

And for the curious, I’m using Tower to handle my Git commits — command lines are not my cup of tea. Tower is a very nice Git GUI that integrates seamlessly with GitHub. I’m new to Tower, but so far I like it very much.

SCORM Tip: Use an onunload handler

SCORM courses use JavaScript to send data to the LMS. This data then sits in the browser until the LMS writes it to the database (usually via AJAX or form posts). As previously discussed, invoking commit (save) will ensure the LMS actually writes this data to a database.

But what happens if the browser window containing your course is closed by the learner before the course finishes sending data to the LMS? If you’re not careful about how you’ve coded your course, you can lose some of the data. For example, if a learner completes the course and then immediately closes the window, the course might not have had enough time to tell the LMS about the completion, preventing the completion from appearing in the learner’s record.

It’s best to be proactive about this by setting up an event handler for the browser’s unload and onbeforeunload events. Whatever code you place in these events will execute when the browser is closed. In a SCORM course, you should place a commit (save) and terminate (quit) command in these events to ensure the SCORM data is properly persisted in the database and the session is properly terminated.

The code is pretty straightforward (this example uses SCORM 2004):


//Assuming API refers to your SCORM API
//and API_isActive returns a boolean indicating
//whether your API has already been terminated

var unloaded = false;
function unloadHandler(){
   if(!unloaded && API_isActive){
      API.SetValue("cmi.exit", "suspend"); //Set exit to whatever is needed
      API.Commit(""); //save all data that has already been sent
      API.Terminate(""); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

Since some browsers support onbeforeunload and others don’t, we use the unloadHandler on both onbeforeunload and onunload, just to be safe. If a browser supports both of these events, the unloaded boolean ensures the scorm.quit function is not executed more than once.

If you’re using the pipwerks SCORM wrapper, your code will be even simpler, because the pipwerks wrapper automatically checks the API’s availability before performing any action. The wrapper also sets the cmi.exit/cmi.core.exit parameter for you.


var unloaded = false;
function unloadHandler(){
   if(!unloaded){
      scorm.save(); //save all data that has already been sent
      scorm.quit(); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

SCORM Tip: Don’t forget to commit!

A number of people have recently asked me about the scorm.save() function in the pipwerks SCORM wrappers. What is it, and when should it be used?

The pipwerks scorm.save() function is a shortcut for SCORM’s Commit (SCORM 2004) and LMSCommit (SCORM 1.2) methods. Invoking commit in SCORM means you are explicitly instructing the LMS to persist the data you’ve sent. In other words, you’re telling the LMS to save your stuff!

When sending data from your course to the LMS, the data is traveling from the course window to the LMS via JavaScript and is stored in the browser. The SCORM spec gives LMSs the flexibility to decide when to transfer this data to the database. Some LMSs will not immediately write the data to the database because it can clog up the system; they prefer to queue up the data and send it in bunches. This explains why some courses will be successful in sending the data from the course to the LMS, but the LMS doesn’t seem to save the data. You didn’t tell it to! I know, I know, it seems daft, but it happens. So, to be safe, ensure your data gets saved by instructing the LMS to save the data.

Note that some LMSs will automatically commit at the end of a session, usually by detecting if the course window was closed (window.onunload) or if a certain CMI call was invoked, such as setting the completion status. However, you should never take your chances, and should design your SCO to commit regularly. Just be careful not to overdo it.

The rule of thumb is to invoke a commit (save()) if you use the pipwerks wrapper) after a significant chunk of data has been sent to the LMS, but not after each and every call:

NO (committing too frequently):


scorm.set("cmi.location", "some string indicating location");
scorm.save();
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.save();
scorm.set("cmi.score.raw", 80);
scorm.save();

NO (not committing enough):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);

YES (one commit after a short series of ‘set’ calls):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);
scorm.save();

Ideas wanted for new SCORM wrappers

As you may have read in previous posts or tweets, I’m working on a new SCORM 2004 wrapper for both JavaScript and ActionScript that will contain advanced functionality and improved shortcuts.

For instance, I’m trying to write an easier way to work with the cmi.interactions model, and also trying to add more error-checking that will look for gotchas such as exceeding the length limit of suspend_data.

I’m looking for good ideas. How do you handle your cmi.interactions? What kind of code shortcuts would you like to see? How can working with SCORM be made easier? I’d love to hear your ideas, just post them as comments below or send them to me on twitter.

This project — just like my previous wrappers — will be freeware, either MIT license or GNU license, so no worries about me running off and selling your ideas!

(FYI: for now I’m focusing on SCORM 2004 — SCORM 1.2 should be retired — but depending on how things work out I might add backwards-compatibility for SCORM 1.2.)

SCORM resources

I recently emailed a shortlist of good SCORM development resources to a colleague, and figured I should probably post a list here, too. This is a quickie list, and I’m sure I’m leaving someone out. If you know of any resources I’ve missed, please add a link in the comments. This list is presented in no particular order.

  • Claude Ostyn’s site. He passed away in 2007, so there haven’t been any updates since then. His materials present a nice overview including lots of examples.
  • The ADL website. Their SCORM Documentation Suite is the official documentation. Must-have for any SCORM developer.
  • Redbird DevNet [link no longer available]. They have a nice tutorial/walk-through of SCORM 2004 architecture.
  • Rustici Software. A for-profit business that works almost exclusively with SCORM. They’ve posted some helpful resources on their site.
  • Aaron Silvers has been writing about SCORM (especially with regards to Flash) for a long time.
  • adlCommunity. A site dedicated to advancing ADL’s technologies. There are some good resources for SCORM developers, including an overview of SCORM written by the late Philip Dodds, one of the chief architects of SCORM.
  • Academic ADL Co-Lab. An offshoot of the ADL that offers SCORM resources and training for would-be SCORM developers. Also hosts Joe Nelson’s custom SCORM JavaScript framework LibSCORM (“a boilerplate template that implements common SCO Tracking and Communication functionality”), which some may find useful.

Of course, I also have a few SCORM odds and ends on my site you may find useful:

For those of you who don’t know, SCORM 2.0 is in the works, and is being handled by Learning Education Training Systems Interoperability (LETSI). SCORM 2.0 is still in the formative stages and won’t be ready for a few years (minimum), but you can certainly join the conversation and help mold SCORM 2.0 by visiting the LETSI site.

Update: LETSI is no longer tasked with SCORM 2.0, which will remain with the ADL. LETSI will continue to work on e-learning technology standards, including a potential replacement for SCORM.