Player Wrapper Guide
This guide explains how to build a player UI that responds to SCORM events using scorm-again. Whether you're building a new player from scratch or integrating with an existing system, this guide provides production-ready patterns and working examples.
Table of Contents
- Overview
- Quick Start
- Architecture
- Part 1: Building from Scratch
- Part 2: Integrating with Existing Players
- Reference
- Demo Setup
Overview
A SCORM player wrapper handles:
- Content Frame: iframe where SCOs run
- Navigation Controls: prev/next/exit buttons
- Course Menu: TOC with status indicators
- Progress Display: completion %, scores
- Event Handling: responding to API calls
Which Approach?
| Scenario | Recommended Approach |
|---|---|
| Building new player, SCORM 1.2 | Part 1: SCORM 1.2 Multi-SCO |
| Building new player, SCORM 2004 (no sequencing) | Part 1: SCORM 2004 Simple |
| Building new player, SCORM 2004 (with sequencing) | Part 1: SCORM 2004 Sequenced |
| Adding to existing player | Part 2: Event-Driven Integration |
Quick Start
30-Second Integration
For existing players, use the event-driven approach:
import Scorm12API from 'scorm-again';
const api = new Scorm12API({ autocommit: true });
window.API = api;
// Listen for status changes
api.on('LMSSetValue.cmi.core.lesson_status', (element, value) => {
updateMenuIcon(currentScoId, value);
updateProgressBar();
});
// Listen for score changes
api.on('LMSSetValue.cmi.core.score.raw', (element, value) => {
updateScoreDisplay(value);
});
// Listen for finish
api.on('LMSFinish', () => {
determineNextSco();
});Architecture
Player Components
┌─────────────────────────────────────────────────────────────────┐
│ Player Wrapper │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Header │ │ Navigation │ │ Progress Bar │ │
│ │ (title, │ │ (prev/next, │ │ (completion %, │ │
│ │ status) │ │ exit) │ │ score display) │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
│ ┌──────────────────────────────────┐ ┌────────────────────┐ │
│ │ │ │ Course Menu │ │
│ │ Content Frame │ │ (TOC/outline, │ │
│ │ (iframe for SCO) │ │ SCO status │ │
│ │ │ │ indicators) │ │
│ └──────────────────────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Event Flow
SCO calls LMSSetValue('cmi.core.lesson_status', 'passed')
│
▼
scorm-again fires 'LMSSetValue' event
│
▼
Player listens: api.on('LMSSetValue', handler)
│
▼
Handler updates state and UI
│
├──► Update menu item icon
├──► Update progress bar
└──► Persist stateData Flow Architecture
┌────────────────┐ ┌──────────────┐ ┌────────────┐
│ SCO Content │────────►│ SCORM API │────────►│ LMS │
│ (iframe) │ API │ (window.API) │ HTTP │ Backend │
└────────────────┘ Calls └──────────────┘ POST └────────────┘
│
│ Events
▼
┌──────────────┐
│ Player │
│ Wrapper │
│ (UI/State) │
└──────────────┘
│
├──► Update Menu
├──► Update Progress
├──► Enable/Disable Nav
└──► Persist to localStoragePart 1: Building from Scratch
SCORM 1.2 Multi-SCO Player
This section demonstrates building a complete SCORM 1.2 multi-SCO player from scratch.
Step 1: HTML Structure
The player HTML provides the container structure for all UI components. See the complete HTML in demos/player-wrapper/scorm12-multi-sco/index.html.
Key elements:
- Header with title and status badge
- Progress bar showing completion percentage
- Course menu (navigation sidebar)
- Content iframe for SCO delivery
- Navigation buttons (previous, next, exit)
- Completion overlay
Step 2: Course Manifest
Define your course structure (in production, parse from imsmanifest.xml):
const COURSE_MANIFEST = {
id: 'scorm12-demo-course',
title: 'SCORM 1.2 Demo Course',
scos: [
{
id: 'sco1',
title: 'Introduction',
launchUrl: './content/sco1/index.html',
masteryScore: 80,
},
{
id: 'sco2',
title: 'Core Concepts',
launchUrl: './content/sco2/index.html',
masteryScore: 80,
},
{
id: 'sco3',
title: 'Assessment',
launchUrl: './content/sco3/index.html',
masteryScore: 70,
},
],
};Step 3: State Management
Create a state manager to track SCO progress:
class PlayerState {
constructor() {
this.scoStates = new Map();
this.currentScoId = null;
this.courseId = COURSE_MANIFEST.id;
}
getScoState(scoId) {
if (!this.scoStates.has(scoId)) {
this.scoStates.set(scoId, {
lessonStatus: 'not attempted',
score: { raw: null, min: 0, max: 100 },
lessonLocation: '',
suspendData: '',
totalTime: '0000:00:00.00',
sessionTime: '0000:00:00.00',
hasBeenLaunched: false,
});
}
return this.scoStates.get(scoId);
}
updateScoState(scoId, updates) {
const current = this.getScoState(scoId);
const updated = { ...current, ...updates };
this.scoStates.set(scoId, updated);
this.persist();
}
persist() {
const data = {
scoStates: Object.fromEntries(this.scoStates),
currentScoId: this.currentScoId,
timestamp: Date.now(),
};
localStorage.setItem(`scorm-state-${this.courseId}`, JSON.stringify(data));
}
restore() {
const stored = localStorage.getItem(`scorm-state-${this.courseId}`);
if (stored) {
const data = JSON.parse(stored);
this.scoStates = new Map(Object.entries(data.scoStates));
this.currentScoId = data.currentScoId;
}
}
calculateRollup() {
let completedCount = 0;
let scoreSum = 0;
let scoreCount = 0;
for (const sco of COURSE_MANIFEST.scos) {
const state = this.getScoState(sco.id);
if (['completed', 'passed', 'failed'].includes(state.lessonStatus)) {
completedCount++;
}
if (state.score.raw !== null) {
scoreSum += parseFloat(state.score.raw);
scoreCount++;
}
}
const completionPct = Math.round((completedCount / COURSE_MANIFEST.scos.length) * 100);
const avgScore = scoreCount > 0 ? Math.round(scoreSum / scoreCount) : null;
let overallStatus = 'not attempted';
if (completedCount > 0) overallStatus = 'incomplete';
if (completedCount === COURSE_MANIFEST.scos.length) {
overallStatus = avgScore !== null && avgScore >= 70 ? 'passed' : 'completed';
}
return { completedCount, completionPct, avgScore, overallStatus };
}
}Step 4: Initialize SCORM API with Event Listeners
import Scorm12API from 'scorm-again';
class Scorm12Player {
constructor() {
this.state = new PlayerState();
this.api = null;
this.manifest = COURSE_MANIFEST;
}
init() {
this.state.restore();
this.initApi();
this.buildMenu();
this.updateProgress();
this.setupUiListeners();
this.autoLaunch();
}
initApi() {
this.api = new Scorm12API({
autocommit: true,
autocommitSeconds: 30,
logLevel: 4,
});
window.API = this.api;
// Set up API event handlers
this.api.on('LMSInitialize', () => this.handleInitialize());
this.api.on('LMSSetValue', (element, value) => this.handleSetValue(element, value));
this.api.on('LMSCommit', () => this.handleCommit());
this.api.on('LMSFinish', () => this.handleFinish());
}
handleSetValue(element, value) {
if (!this.state.currentScoId) return;
const scoId = this.state.currentScoId;
switch (element) {
case 'cmi.core.lesson_status':
this.state.updateScoState(scoId, { lessonStatus: value });
this.updateMenuItem(scoId);
this.updateProgress();
break;
case 'cmi.core.score.raw':
const currentState = this.state.getScoState(scoId);
this.state.updateScoState(scoId, {
score: { ...currentState.score, raw: value },
});
this.updateMenuItem(scoId);
this.updateProgress();
break;
case 'cmi.core.lesson_location':
this.state.updateScoState(scoId, { lessonLocation: value });
break;
case 'cmi.suspend_data':
this.state.updateScoState(scoId, { suspendData: value });
break;
}
}
handleFinish() {
if (!this.state.currentScoId) return;
const scoId = this.state.currentScoId;
const exit = this.api.cmi?.core?.exit || '';
// Check if course is complete
const rollup = this.state.calculateRollup();
if (rollup.completedCount === rollup.totalCount) {
this.showCompletionScreen(rollup);
return;
}
// Auto-advance to next SCO
if (exit !== 'suspend' && exit !== 'logout') {
const nextSco = this.getNextSco(scoId);
if (nextSco) {
setTimeout(() => this.launchSco(nextSco.id), 500);
}
}
}
}Step 5: SCO Launch and Navigation
launchSco(scoId) {
const sco = this.manifest.scos.find((s) => s.id === scoId);
if (!sco) return;
const existingState = this.state.getScoState(scoId);
// Load CMI data into API
this.api.loadFromJSON({
cmi: {
core: {
student_id: 'demo-student-001',
student_name: 'Demo Student',
lesson_status: existingState.lessonStatus,
lesson_location: existingState.lessonLocation,
score: {
raw: existingState.score.raw?.toString() || '',
min: existingState.score.min?.toString() || '0',
max: existingState.score.max?.toString() || '100',
},
credit: 'credit',
entry: existingState.hasBeenLaunched ? 'resume' : 'ab-initio',
},
suspend_data: existingState.suspendData,
student_data: {
mastery_score: sco.masteryScore?.toString() || '',
},
},
});
this.state.updateScoState(scoId, { hasBeenLaunched: true });
this.state.currentScoId = scoId;
document.getElementById('sco-frame').src = sco.launchUrl;
this.updateNavButtons();
}SCORM 2004 Simple Multi-SCO Player
For SCORM 2004 without sequencing, the implementation is similar with key differences:
Key Differences from SCORM 1.2
- API Object Name: Use
window.API_1484_11instead ofwindow.API - Separate Completion and Success: Track
completion_statusandsuccess_statusseparately - Scaled Scores: Use
cmi.score.scaled(-1 to 1) in addition tocmi.score.raw - Time Format: Use ISO 8601 duration (PT1H30M45S) instead of HHHH:MM:SS.CC
- Navigation Requests: Handle
adl.nav.requestfor SCO-initiated navigation
import Scorm2004API from 'scorm-again/Scorm2004API';
class Scorm2004SimplePlayer {
initApi() {
this.api = new Scorm2004API({
autocommit: true,
autocommitSeconds: 30,
});
window.API_1484_11 = this.api;
this.api.on('Initialize', () => this.handleInitialize());
this.api.on('SetValue', (element, value) => this.handleSetValue(element, value));
this.api.on('Terminate', () => this.handleTerminate());
}
handleSetValue(element, value) {
if (!this.state.currentScoId) return;
const scoId = this.state.currentScoId;
switch (element) {
case 'cmi.completion_status':
this.state.updateScoState(scoId, { completionStatus: value });
this.updateMenuItem(scoId);
this.updateProgress();
break;
case 'cmi.success_status':
this.state.updateScoState(scoId, { successStatus: value });
this.updateMenuItem(scoId);
break;
case 'cmi.score.scaled':
const state = this.state.getScoState(scoId);
this.state.updateScoState(scoId, {
score: { ...state.score, scaled: value },
});
this.updateMenuItem(scoId);
this.updateProgress();
break;
case 'adl.nav.request':
// The request is not actionable until the SCO ends its attempt.
this.pendingNavRequest = value;
break;
}
}
handleTerminate() {
if (this.pendingNavRequest) {
this.handleNavRequest(this.pendingNavRequest);
this.pendingNavRequest = null;
}
}
handleNavRequest(request) {
switch (request) {
case 'continue':
case '_continue':
this.launchNext();
break;
case 'previous':
case '_previous':
this.launchPrevious();
break;
case 'exit':
case 'exitAll':
this.exitCourse();
break;
}
}
}SCORM 2004 Sequenced Player
For courses with SCORM 2004 sequencing, use the built-in sequencing engine:
When reusing an API instance, call reset(undefined, { preserveListeners: true, resetTotalTime: true }) between SCO deliveries. This keeps listeners registered with api.on() and clears the outgoing SCO's total time. Load the incoming SCO's saved total before its Initialize() call; a SCO without saved time starts at PT0S. Callbacks configured through sequencing.eventListeners also survive the API reset.
These options apply to one reset call. Without them, reset() retains its existing behavior: it clears on() listeners and preserves total time. When using preserveListeners, register the handlers once rather than adding them again after each reset.
The example below keeps each activity's total in memory for the demo. In an LMS, persist and load that value with the SCO's other runtime data for the learner and attempt; sequencing state does not replace this SCO-local storage.
Sequencing Configuration
const COURSE_MANIFEST = {
id: 'scorm2004-sequenced-demo',
title: 'SCORM 2004 Sequenced Course',
activities: [
{
id: 'root',
title: 'Course Root',
isContainer: true,
children: [
{
id: 'module1',
title: 'Module 1: Fundamentals',
isContainer: true,
children: [
{
id: 'module1-lesson1',
title: 'Lesson 1: Introduction',
launchUrl: './content/module1/lesson1/index.html',
},
{
id: 'module1-lesson2',
title: 'Lesson 2: Basics',
launchUrl: './content/module1/lesson2/index.html',
},
],
},
{
id: 'module2',
title: 'Module 2: Advanced',
isContainer: true,
children: [
{
id: 'module2-lesson1',
title: 'Lesson 1: Deep Dive',
launchUrl: './content/module2/lesson1/index.html',
},
],
},
],
},
],
};Initialize API with Sequencing
import { Scorm2004API } from 'scorm-again/scorm2004';
class Scorm2004SequencedPlayer {
initApi() {
this.totalTimeByActivity = new Map();
this.hasDeliveredSco = false;
this.api = new Scorm2004API({
autocommit: true,
renderCommonCommitFields: true,
sequencing: {
activityTree: COURSE_MANIFEST.activities[0],
autoRollupOnCMIChange: false,
eventListeners: {
onActivityDelivery: (activity) => this.handleActivityDelivery(activity),
onNavigationValidityUpdate: (data) => this.handleNavValidityUpdate(data),
onRollupComplete: (activity) => this.handleRollupComplete(activity),
onSequencingSessionEnd: (data) => this.handleSessionEnd(data),
},
},
});
window.API_1484_11 = this.api;
this.setupApiListeners();
}
setupApiListeners() {
this.api.on('Initialize', () => this.handleInitialize());
this.api.on('SetValue', (el, val) => this.handleSetValue(el, val));
this.api.on('Terminate', () => this.handleTerminate());
}
handleActivityDelivery(activity) {
if (this.hasDeliveredSco) {
this.saveCurrentActivityTime();
this.api.reset(undefined, { preserveListeners: true, resetTotalTime: true });
}
this.currentActivityId = activity.id;
this.api.loadFromJSON({
cmi: { total_time: this.totalTimeByActivity.get(activity.id) ?? 'PT0S' },
});
this.hasDeliveredSco = true;
const activityDef = this.findActivity(activity.id);
if (activityDef?.launchUrl) {
document.getElementById('sco-frame').src = activityDef.launchUrl;
}
this.highlightCurrentActivity(activity.id);
}
saveCurrentActivityTime() {
if (this.currentActivityId) {
this.totalTimeByActivity.set(
this.currentActivityId,
this.api.cmi.getCurrentTotalTime(),
);
}
}
handleNavValidityUpdate(data) {
document.getElementById('btn-next').disabled = !data.continue;
document.getElementById('btn-prev').disabled = !data.previous;
this.choiceTargets = Object.keys(data.choice)
.filter((id) => data.choice[id] === 'true');
}
handleRollupComplete(activity) {
this.updateMenuItemStatus(activity.id);
this.updateProgress();
}
handleSessionEnd(data) {
this.saveCurrentActivityTime();
this.currentActivityId = null;
if (data.reason === 'satisfied') {
this.showCompletionScreen({ completionPct: 100 });
}
}
}Part 2: Integrating with Existing Players
If you already have a player UI, use the event-driven integration pattern:
Event-Driven Integration Pattern
import Scorm12API from 'scorm-again';
const api = new Scorm12API({
autocommit: true,
lmsCommitUrl: '/api/scorm/commit',
});
window.API = api;
let currentScoId = null;
// Listen for all SetValue calls
api.on('LMSSetValue', (element, value) => {
console.log(`SetValue: ${element} = ${value}`);
if (element === 'cmi.core.lesson_status') {
yourPlayer.updateStatus(currentScoId, value);
} else if (element === 'cmi.core.score.raw') {
yourPlayer.updateScore(currentScoId, value);
}
});
// Listen for specific elements
api.on('LMSSetValue.cmi.core.lesson_status', (element, value) => {
yourPlayer.updateMenuIcon(currentScoId, value);
yourPlayer.recalculateProgress();
});
// Listen for finish
api.on('LMSFinish', () => {
yourPlayer.onScoComplete(currentScoId);
});
function launchSco(scoId, scoData) {
currentScoId = scoId;
api.loadFromJSON({
cmi: {
core: {
student_id: scoData.studentId,
student_name: scoData.studentName,
lesson_status: scoData.lessonStatus || 'not attempted',
entry: scoData.hasBeenLaunched ? 'resume' : 'ab-initio',
},
},
});
yourPlayer.loadContentInFrame(scoData.launchUrl);
}Framework Integration Examples
React Integration
import { useEffect, useRef, useState } from 'react';
import Scorm12API from 'scorm-again';
function ScormPlayer({ course }) {
const [progress, setProgress] = useState({ completionPct: 0 });
const [menuItems, setMenuItems] = useState([]);
const apiRef = useRef(null);
const currentScoId = useRef(null);
useEffect(() => {
apiRef.current = new Scorm12API({ autocommit: true });
window.API = apiRef.current;
const statusHandler = (element, value) => {
setMenuItems(prev => prev.map(item =>
item.id === currentScoId.current
? { ...item, status: value }
: item
));
recalculateProgress();
};
apiRef.current.on('LMSSetValue.cmi.core.lesson_status', statusHandler);
return () => {
apiRef.current.off('LMSSetValue.cmi.core.lesson_status', statusHandler);
};
}, []);
const recalculateProgress = () => {
const completed = menuItems.filter(item =>
['completed', 'passed', 'failed'].includes(item.status)
).length;
const completionPct = Math.round((completed / menuItems.length) * 100);
setProgress({ completionPct });
};
return (
<div className="scorm-player">
<div className="progress-bar">
<div style={{ width: `${progress.completionPct}%` }} />
<span>{progress.completionPct}% Complete</span>
</div>
<iframe title="SCO Content" />
</div>
);
}Vue Integration
<template>
<div class="scorm-player">
<div class="progress-bar">
<div :style="{ width: progress.completionPct + '%' }" />
<span>{{ progress.completionPct }}% Complete</span>
</div>
<iframe ref="frame" title="SCO Content" />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Scorm12API from 'scorm-again';
const progress = ref({ completionPct: 0 });
const menuItems = ref([]);
let api = null;
onMounted(() => {
api = new Scorm12API({ autocommit: true });
window.API = api;
api.on('LMSSetValue.cmi.core.lesson_status', (element, value) => {
// Update menu items and recalculate progress
recalculateProgress();
});
});
onUnmounted(() => {
if (api) api.LMSFinish('');
});
const recalculateProgress = () => {
const completed = menuItems.value.filter(item =>
['completed', 'passed', 'failed'].includes(item.status)
).length;
progress.value.completionPct = Math.round((completed / menuItems.value.length) * 100);
};
</script>Reference
Event to UI Mapping
SCORM 1.2 Events
| Event | CMI Element | UI Update | Example |
|---|---|---|---|
LMSInitialize | - | Hide loading indicator | loading.hidden = true |
LMSSetValue | cmi.core.lesson_status | Update menu icon, progress | updateMenuIcon() |
LMSSetValue | cmi.core.score.raw | Update score display | updateScore() |
LMSCommit | - | Recalculate rollup | updateProgress() |
LMSFinish | - | Navigate to next SCO | nextSco() |
SCORM 2004 Events
| Event | CMI Element | UI Update |
|---|---|---|
Initialize | - | Hide loading indicator |
SetValue | cmi.completion_status | Update menu icon, progress |
SetValue | cmi.success_status | Update badge |
SetValue | cmi.score.scaled | Update score (convert to %) |
SetValue | adl.nav.request | Handle navigation request |
Terminate | - | Navigate to next SCO |
SCORM 2004 Sequencing Events
| Event | Parameters | UI Update |
|---|---|---|
onActivityDelivery | activity | Load SCO, highlight in menu |
onNavigationValidityUpdate | { continue, previous, choice, jump, hideLmsUi, auxiliaryResources } | Enable/disable nav buttons and update delivery UI |
onRollupComplete | activity | Update menu status, progress |
onSequencingSessionEnd | { reason } | Show completion screen |
Troubleshooting
Issue: SCO not initializing
Symptoms: SCO shows blank screen, no API calls logged
Solutions:
- SCORM 1.2: Ensure
window.APIis set before loading content - SCORM 2004: Ensure
window.API_1484_11is set - Check console for errors
- Verify iframe sandbox permissions include
allow-scripts
Issue: Menu not updating
Symptoms: Complete SCO but menu icon stays gray
Solutions:
- Verify event listener is registered:
api.on('LMSSetValue.cmi.core.lesson_status', handler) - Check that
currentScoIdis being tracked - Ensure DOM query selector finds the correct element
- Add console.log to verify event is firing
Issue: Navigation buttons always disabled
Symptoms: Can't navigate even after completing SCO
Solutions:
- Call
updateNavButtons()after launching each SCO - For sequencing, ensure
onNavigationValidityUpdatelistener is configured - Check button disabled logic matches current index
Issue: Progress not calculating
Symptoms: Progress bar shows 0% after completing SCOs
Solutions:
- Verify state is being updated in
handleSetValue - Check rollup calculation logic includes all statuses:
['completed', 'passed', 'failed'] - Call
updateProgress()after every status change - Add logging to
calculateRollup()to debug
Demo Setup
Running the Demos Locally
# Clone and install
git clone https://github.com/jcputney/scorm-again.git
cd scorm-again
npm install
# Build the library
npm run build
# Navigate to demos
cd demos/player-wrapper
# Start demo server
npm start
# Open in browser
# SCORM 1.2: http://localhost:3000/scorm12-multi-sco/
# SCORM 2004 Simple: http://localhost:3000/scorm2004-simple/
# SCORM 2004 Sequenced: http://localhost:3000/scorm2004-sequenced/Demo File Structure
demos/player-wrapper/
├── shared/
│ ├── player-core.css # Shared CSS
│ ├── player-core.js # Shared utilities
│ └── mock-sco/ # Sample SCO content
├── scorm12-multi-sco/
│ ├── index.html # Player HTML
│ ├── player.js # SCORM 1.2 implementation
│ └── sample-course/ # Demo course
├── scorm2004-simple/
│ ├── index.html # Player HTML
│ ├── player.js # SCORM 2004 simple
│ └── sample-course/ # Demo course
└── scorm2004-sequenced/
├── index.html # Player HTML
├── player.js # SCORM 2004 sequenced
└── sample-course/ # Demo with hierarchyExploring the Demos
Each demo includes:
- Complete player implementation - Fully functional SCORM player
- Sample course content - Mock SCOs demonstrating common patterns
- Event logging - Console logs showing all API calls
- State persistence - localStorage-based state management
- Responsive design - Works on desktop and mobile
Key Features to Explore:
- SCORM 1.2 Multi-SCO: Simple manual navigation, status rollup
- SCORM 2004 Simple: 2004 data model without sequencing
- SCORM 2004 Sequenced: Full sequencing engine with choice navigation
Using Demos as Templates
To use a demo as a starting point:
- Copy the demo directory to your project
- Update the
COURSE_MANIFESTto match your course structure - Modify CSS to match your design
- Add your LMS integration (commit endpoint, etc.)
- Customize UI components as needed
Related Documentation
- API Events Reference - Complete event system documentation
- LMS Integration Guide - Core LMS integration concepts
- SCORM 1.2 Multi-SCO Guide - Multi-SCO utilities
- Cross-Frame Communication Guide - Sandboxed iframe integration
- Data Requirements Quick Reference - Data model reference
Summary
This guide covered:
- Architecture - Understanding player components and event flow
- Building from Scratch - Complete implementations for all SCORM versions
- Integrating with Existing Players - Event-driven patterns and framework examples
- Reference - Event mappings, troubleshooting, and best practices
- Demos - Running and exploring working examples
Key Takeaways:
- Use event listeners to respond to SCORM API calls
- Track state separately from the API for UI updates
- Handle SCORM 1.2 and 2004 differences (API name, data model, time format)
- For sequencing, rely on sequencing events rather than manual logic
- Persist state to localStorage for resume capability
- Test with real SCORM content to validate compliance
For additional help, see the troubleshooting section or explore the working demos.