Forum Discussion

Ainsley_Burnett's avatar
Ainsley_Burnett
Community Member
2 days ago

Idle timer in Storyline 360

I need help with an idle timer that uses JavaScript. The problem is that the idle popup is showing up when a user pauses a video in the course, even though it hasn't reached 300 seconds. Here is what I have (the actual course is in Docebo, if that makes a difference): 

On the idle master: 

 

 

The JavaScript on the master layer is this: if(window.maybeSuspendOnIdle){window.maybeSuspendOnIdle();}

The Idle Loop has a LoopTick that adds 1s like this:

The Idle Popup layer looks like this:

 

The first JavaScript is: sendResumed();

The second JavaScript is: // Resume Storyline timeline from popup layer
try { GetPlayer().ResumeState(); } catch(e){}

There is also a first slide that runs the following:

/* ===== Storyline Idle + Docebo xAPI + Media-Aware Starter (NO mousemove) ===== */
(function(){
  if (window.__IdleAllInOne) { console.log("[IdleXAPI] already initialized"); return; }
  window.__IdleAllInOne = true;

  var IDLE_THRESHOLD_SECS = 300;     // set to 10 for quick testing, then back to 300
  var LOG_PREFIX = "[IdleXAPI]";

  function getPlayer() { try { return GetPlayer(); } catch(e){ return null; } }
  function log(){ try { console.log.apply(console, [LOG_PREFIX].concat([].slice.call(arguments))); } catch(e){} }
  function setVar(name, val){ var p=getPlayer(); if (!p) return; try { p.SetVar(name, val); } catch(e){} }
  function getVar(name){ var p=getPlayer(); if (!p) return null; try { return p.GetVar(name); } catch(e){ return null; } }

  /* ------------------ xAPI helpers ------------------ */
  (function(){
    if (window.__IdleXAPI) return; window.__IdleXAPI = { sentSuspend:false, lastStatement:null };

    function findTinCan(){
      var ctx=window, hops=0;
      while (ctx && hops<5){
        if (ctx.tincan) return { type:"tincan", api:ctx.tincan, win:ctx };
        if (ctx.TinCan) { if (ctx.tincan) return { type:"tincan", api:ctx.tincan, win:ctx }; return { type:"TinCan", api:ctx.TinCan, win:ctx }; }
        if (ctx===ctx.parent) break; ctx = ctx.parent; hops++;
      }
      return null;
    }
    function getActor(tcWrap){
      try{
        if (tcWrap && tcWrap.api && typeof tcWrap.api.getContext === "function") {
          var c = tcWrap.api.getContext(); if (c && c.actor) return c.actor;
        }
        if (tcWrap && tcWrap.api && tcWrap.api.actor) return tcWrap.api.actor;
      }catch(e){}
      return { name:"Learner", mbox:"mailto:noreply@example.com" };
    }
    function activityId(){
      var v = getVar("CourseActivityId");
      if (v && String(v).trim().length) return String(v).trim();
      return location.href.split("#")[0];
    }
    function buildStatement(verbId, display){
      var tc = findTinCan();
      var stmt = {
        actor: getActor(tc),
        verb: { id: verbId, display: { "en-US": display } },
        object: {
          id: activityId(),
          definition: { name: { "en-US": document.title || "Storyline Course" },
                        type: "http://adlnet.gov/expapi/activities/course" },
          objectType: "Activity"
        },
        timestamp: new Date().toISOString()
      };
      return { tc: tc, stmt: stmt };
    }
    function sendWithTinCan(tcWrap, stmt, cb){
      try{
        if (!tcWrap) throw new Error("No TinCan context");
        if (tcWrap.type === "tincan" && typeof tcWrap.api.sendStatement === "function") {
          tcWrap.api.sendStatement(stmt, function(err, xhr){
            if (err) log("sendStatement error:", err);
            else log("sendStatement ok:", xhr && xhr.status);
            cb && cb(!err);
          });
          return;
        }
        log("TinCan present but not configured; skipping send.");
        cb && cb(false);
      }catch(e){ log("sendWithTinCan exception:", e.message); cb && cb(false); }
    }
    function sendStatement(verbId, display, cb){
      var pack = buildStatement(verbId, display);
      window.__IdleXAPI.lastStatement = pack.stmt;
      if (!pack.tc) { log("No tincan/TinCan context; skipping xAPI send."); cb && cb(false); return; }
      sendWithTinCan(pack.tc, pack.stmt, cb);
    }

    window.maybeSuspendOnIdle = function(){
      var isIdle = !!getVar("IsIdle");
      if (isIdle && !window.__IdleXAPI.sentSuspend) {
        sendStatement("http://adlnet.gov/expapi/verbs/suspended", "suspended", function(ok){
          if (ok) window.__IdleXAPI.sentSuspend = true;
        });
      }
    };
    window.sendResumed = function(){
      sendStatement("http://adlnet.gov/expapi/verbs/resumed", "resumed", function(ok){
        if (ok) window.__IdleXAPI.sentSuspend = false;
      });
    };
  })();

  /* ------------------ Timeline-independent idle timer ------------------ */
  (function(){
    if (window.__IdleTickInterval) return;
    function tick(){
      var isIdle = !!getVar("IsIdle");
      var idleTime = parseInt(getVar("IdleTime"), 10) || 0;
      if (!isIdle) {
        setVar("IdleTime", ++idleTime);
        if (idleTime >= IDLE_THRESHOLD_SECS) setVar("IsIdle", true);
      }
    }
    window.__IdleTickInterval = setInterval(tick, 1000);
    log("JS idle interval running (independent of slide timeline).");
  })();

  /* ------------------ Global interaction listeners (NO mousemove) ------------------ */
  (function(){
    if (window.__IdleInputHooked_NoMouseMove) return;
    window.__IdleInputHooked_NoMouseMove = true;

    function resetIdle(){ setVar("IdleTime", 0); setVar("IsIdle", false); }

    // Intentionally excluding 'mousemove'
    ["mousedown","keydown","touchstart","pointerdown","wheel","click"].forEach(function(evt){
      window.addEventListener(evt, resetIdle, { passive:true });
    });
    log("Global input listeners attached (no mousemove).");
  })();

  /* ------------------ Media-aware resets for <video>/<audio> ------------------ */
  (function(){
    if (window.__IdleMediaHooked) return;
    window.__IdleMediaHooked = true;

    function resetIdle(){ setVar("IdleTime", 0); setVar("IsIdle", false); }

    function hook(el){
      if (!el || el.__idleHooked) return;
      el.__idleHooked = true;
      ["play","playing","timeupdate","seeking","seeked","ratechange","volumechange"].forEach(function(evt){
        el.addEventListener(evt, resetIdle, { passive:true });
      });
    }

    function scan(){
      try { Array.from(document.querySelectorAll("video,audio")).forEach(hook); } catch(e){}
    }
    scan();
    var obs = new MutationObserver(scan);
    obs.observe(document.documentElement, { childList:true, subtree:true });

    log("Media-aware reset attached.");
  })();

  log("All-in-one Idle + xAPI injector initialized (mousemove ignored).");
})();

It took me forever to get this working, and I don't know where the error is. It was all working when I previewed, but now learners are reporting that it doesn’t always take the full five minutes before the Idle Popup shows up. Where am I going wrong?

Any help at all would be hugely appreciated!

No RepliesBe the first to reply