Skip to content

Commit

Permalink
scorm player pacakge
Browse files Browse the repository at this point in the history
  • Loading branch information
victazzz committed Feb 18, 2025
1 parent c747998 commit 96885d8
Show file tree
Hide file tree
Showing 2 changed files with 12 additions and 226 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@escolalms/gift-pegjs": "^0.2.8",
"@escolalms/h5p-react": "^0.2.18",
"@escolalms/markdown-editor": "^12.0.6",
"@escolalms/scorm-player": "^0.0.0",
"@escolalms/ts-models": "^0.0.35",
"@sentry/cli": "^2.40.0",
"@sentry/esbuild-plugin": "^2.22.7",
Expand Down Expand Up @@ -100,8 +101,7 @@
"react-player": "^2.14.1",
"react-sortable-hoc": "^2.0.0",
"react-tiny-oembed": "^1.1.0",
"reportbro-designer": "^3.6.0",
"scorm-again": "^2.6.0"
"reportbro-designer": "^3.6.0"
},
"devDependencies": {
"@ant-design/pro-cli": "^3.3.0",
Expand Down
234 changes: 10 additions & 224 deletions src/components/Scorm/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,235 +1,21 @@
import React, { useEffect, useState } from 'react';
import { AICC, Scorm12API, Scorm2004API } from 'scorm-again';

import { ScormPreview as ScormPlayer } from '@escolalms/scorm-player';
import React from 'react';
declare const REACT_APP_API_URL: string;

interface ScormSettings {
data: {
entry_url_zip: string;
entry_url: string;
};
}

declare global {
interface Window {
ScormSettings: ScormSettings | undefined;
API: any;
API_1484_11: any;
}
interface ScormPreviewProps {
uuid: string;
}

const ScormPreview: React.FC<{ uuid: string }> = ({ uuid }) => {
const [state, setState] = useState<{
loading: boolean;
iframeUrl: string | null;
error: string | null;
}>({
loading: false,
iframeUrl: null,
error: null,
});

const ScormPreview: React.FC<ScormPreviewProps> = ({ uuid }) => {
const API_URL = REACT_APP_API_URL || window.REACT_APP_API_URL;

// Register the Service Worker
const registerServiceWorker = async (
url: string = '/service-worker-scorm.js',
): Promise<ServiceWorkerRegistration | null> => {
try {
const registration = await navigator.serviceWorker.register(url, { scope: '/' });
if (registration.active) {
return registration;
}
return new Promise((resolve) => {
(registration.installing || registration.waiting)?.addEventListener('statechange', () => {
resolve(registration);
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
return null;
}
};

// **Backend Communication Functions**
const post = async (data: any) => {
console.log('TODO: Implement your BACKEND endpoint for set data:', data);
// try {
// const response = await fetch(`${API_URL}/api/scorm/set/${uuid}`, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(data),
// });
// if (!response.ok) throw new Error('Failed to save SCORM data');
// } catch (error) {
// console.error('SCORM Post Error:', error);
// }
return new Promise((resolve, reject) => {
resolve(data);
});
};

const get = async (key: string) => {
console.log('TODO: Implement your BACKEND endpoint for get key:', key);
// try {
// const response = await fetch(`${API_URL}/api/scorm/get/${uuid}/${key}`);
// if (!response.ok) throw new Error('Failed to get SCORM data');
// return await response.json();
// } catch (error) {
// console.error('SCORM Get Error:', error);
// return null;
// }
return new Promise((resolve, reject) => {
resolve(key);
});
};

// **SCORM Initialization Functions**
const initializeScorm12 = (settings: ScormSettings['data']): void => {
window.API = new Scorm12API(settings as any);
console.log('SCORM 1.2 Initialized');

// window.API.on('LMSSetValue.cmi.*', (CMIElement: string, value: any) => {
// post({ cmi: { [CMIElement]: value } });
// });

// window.API.on('LMSGetValue.cmi.*', async (CMIElement: string) => {
// const value = await get(CMIElement);
// window.API.LMSSetValue(CMIElement, value);
// });

// window.API.on('LMSCommit', () => {
// post({ cmi: window.API.cmi });
// });
};

const initializeScorm2004 = (settings: ScormSettings['data']): void => {
window.API_1484_11 = new Scorm2004API(settings as any);
console.log('SCORM 2004 Initialized');

// window.API_1484_11.on('SetValue.cmi.*', (CMIElement: string, value: any) => {
// post({ cmi: { [CMIElement]: value } });
// });

// window.API_1484_11.on('GetValue.cmi.*', async (CMIElement: string) => {
// const value = await get(CMIElement);
// window.API_1484_11.SetValue(CMIElement, value);
// });

// window.API_1484_11.on('Commit', () => {
// post({ cmi: window.API_1484_11.cmi });
// });
};

const initializeAICC = (settings: ScormSettings['data']): void => {
window.API = new AICC(settings as any);
console.log('AICC Initialized');
};

// Load SCORM SCO
const loadScormSCO = async (
scormUuid: string,
registration: ServiceWorkerRegistration,
): Promise<void> => {
try {
const response = await fetch(`${API_URL}/api/scorm/show/${scormUuid}`);
if (!response.ok) throw new Error('Failed to fetch SCORM settings');
const settings: ScormSettings = await response.json();
window.ScormSettings = settings;

const zipUrl = settings.data.entry_url_zip;
const zipExists = await fetch(zipUrl, { method: 'HEAD' });
if (!zipExists.ok) {
// Attempt to create the ZIP if it doesn't exist
console.log('Zip file does not exist:', zipUrl);
const createZip = await fetch(`${API_URL}/api/scorm/zip/${scormUuid}`);
if (!createZip.ok) throw new Error('Failed to create SCORM zip file');
console.log('Zip file created:', zipUrl);
}

// Ask the SW to load the ZIP
registration.active?.postMessage(zipUrl);

navigator.serviceWorker.addEventListener('message', async (event: MessageEvent) => {
const { scormObj } = event.data;
const scormSettings = window.ScormSettings?.data;

if (!scormSettings) {
console.error('SCORM settings are undefined');
return;
}

switch (scormObj.version) {
case '2004':
initializeScorm2004(scormSettings);
break;
case 'AICC':
initializeAICC(scormSettings);
break;
default:
initializeScorm12(scormSettings);
}
// Construct the URL we want to load in the iframe
const potentialIframeUrl = `${scormObj.PREFIX}/${scormSettings.entry_url}`;

// 5) Double-check the file is actually served (avoid “app in app”).
try {
const headCheck = await fetch(potentialIframeUrl, { method: 'HEAD' });
console.log({ headCheck });
if (!headCheck.ok) {
throw new Error(`SCORM entry file missing: ${headCheck.status}`);
}
setState({
...state,
loading: false,
iframeUrl: `${scormObj.PREFIX}/${scormSettings.entry_url}`,
});
} catch (err) {
console.error('SCORM entry file missing or not accessible:', err);
setState({ ...state, error: 'SCORM entry file missing or not accessible.' });
}
});
} catch (error) {
console.error('Failed to load SCORM SCO:', error);
setState({ ...state, error: 'Failed to load SCORM SCO.' });
}
};

// Effect to initialize the component
useEffect(() => {
(async () => {
setState({ ...state, loading: true });

const registration = await registerServiceWorker();
if (!registration) {
console.error('Service Worker registration failed.');
setState({ ...state, error: 'Service Worker registration failed.' });
return;
}
await loadScormSCO(uuid, registration);
})();
}, [uuid]);

if (state.loading) {
return <div style={{ textAlign: 'center', padding: '20px' }}>Loading SCORM...</div>;
}

if (state.error) {
return <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>{state.error}</div>;
}

if (state.iframeUrl) {
return (
<iframe
src={state.iframeUrl}
style={{ width: '100%', height: '1000px' }}
title="SCORM Content"
></iframe>
);
}

// If none of the above, return nothing or a fallback
return null;
return (
<div style={{ height: '100vh' }}>
<ScormPlayer uuid={uuid} apiUrl={API_URL || ''} serviceWorkerUrl="/service-worker-scorm.js" />
</div>
);
};

export default ScormPreview;

0 comments on commit 96885d8

Please sign in to comment.