coursecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * @file video-manager.js
3
+ * @description Singleton manager for video playback in SCORM courses.
4
+ * Handles video for slides and standalone players with position persistence
5
+ * and completion tracking for gating requirements.
6
+ *
7
+ * Features:
8
+ * - Single video instance per context
9
+ * - Position persistence via stateManager
10
+ * - Completion tracking with configurable threshold
11
+ * - Event-based state communication
12
+ * - Native HTML5 video support
13
+ *
14
+ * @author Framework
15
+ * @version 1.0.0
16
+ */
17
+
18
+ import { eventBus } from '../core/event-bus.js';
19
+ import stateManager from '../state/index.js';
20
+ import { logger } from '../utilities/logger.js';
21
+
22
+ /**
23
+ * @typedef {Object} VideoState
24
+ * @property {string|null} currentSrc - Current video source URL
25
+ * @property {string|null} contextId - Current context identifier
26
+ * @property {string} contextType - Type of context ('slide' | 'standalone')
27
+ * @property {number} position - Current playback position in seconds
28
+ * @property {boolean} isPlaying - Whether video is currently playing
29
+ * @property {boolean} isMuted - Whether video is muted
30
+ * @property {number} duration - Total duration of current video
31
+ * @property {number} volume - Volume level (0-1)
32
+ * @property {boolean} required - Whether video completion is required for gating
33
+ * @property {number} completionThreshold - Percentage (0-1) required for completion
34
+ * @property {boolean} isCompleted - Whether video has reached completion threshold
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} VideoConfig
39
+ * @property {string} src - Video file source path (relative to course/assets/)
40
+ * @property {string} [poster] - Poster image path
41
+ * @property {string} [captions] - VTT captions file path
42
+ * @property {boolean} [autoplay=false] - Whether to autoplay when loaded
43
+ * @property {boolean} [required=false] - Whether completion is required for gating
44
+ * @property {number} [completionThreshold=0.95] - Percentage (0-1) required for completion
45
+ */
46
+
47
+ /** Default completion threshold (95%) */
48
+ const DEFAULT_COMPLETION_THRESHOLD = 0.95;
49
+
50
+ class VideoManager {
51
+ constructor() {
52
+ /** @type {boolean} */
53
+ this.isInitialized = false;
54
+
55
+ /** @type {VideoState} */
56
+ this.state = {
57
+ currentSrc: null,
58
+ contextId: null,
59
+ contextType: 'slide',
60
+ position: 0,
61
+ isPlaying: false,
62
+ isMuted: false,
63
+ duration: 0,
64
+ volume: 1,
65
+ required: false,
66
+ completionThreshold: DEFAULT_COMPLETION_THRESHOLD,
67
+ isCompleted: false
68
+ };
69
+
70
+ /** @type {Map<string, number>} - Stores positions for each context */
71
+ this.positionCache = new Map();
72
+
73
+ /** @type {Map<string, boolean>} - Stores completion status for each context */
74
+ this.completionCache = new Map();
75
+
76
+ /** @type {number} - Tracks max position reached (handles seeks/replays) */
77
+ this.maxPositionReached = 0;
78
+
79
+ /** @type {number|null} */
80
+ this.updateInterval = null;
81
+ }
82
+
83
+ /**
84
+ * Initializes the VideoManager. Must be called once during app startup.
85
+ */
86
+ initialize() {
87
+ if (this.isInitialized) {
88
+ logger.warn('[VideoManager] Already initialized');
89
+ return;
90
+ }
91
+
92
+ // Restore persisted state
93
+ this._hydrateFromState();
94
+
95
+ this.isInitialized = true;
96
+ logger.debug('[VideoManager] Initialized');
97
+
98
+ eventBus.emit('video:initialized');
99
+ }
100
+
101
+ /**
102
+ * Attaches event listeners to a video element for state tracking.
103
+ * Unlike AudioManager, VideoManager doesn't own the video element -
104
+ * each video-player component owns its own video element.
105
+ * @param {HTMLVideoElement} video - The video element to attach to
106
+ * @param {string} contextId - The context identifier
107
+ * @param {VideoConfig} config - Video configuration
108
+ */
109
+ attachVideo(video, contextId, config) {
110
+ this._requireInitialized();
111
+
112
+ if (!video || !(video instanceof HTMLVideoElement)) {
113
+ throw new Error('VideoManager.attachVideo: video element is required');
114
+ }
115
+ if (!contextId) {
116
+ throw new Error('VideoManager.attachVideo: contextId is required');
117
+ }
118
+
119
+ // Save position of current context before switching
120
+ if (this.state.contextId && this.state.contextId !== contextId) {
121
+ this._savePosition();
122
+ }
123
+
124
+ // Update state
125
+ this.state.currentSrc = config.src;
126
+ this.state.contextId = contextId;
127
+ this.state.contextType = config.contextType || 'standalone';
128
+ this.state.duration = 0;
129
+ this.state.isPlaying = false;
130
+ this.state.position = 0;
131
+ this.state.required = config.required || false;
132
+ this.state.completionThreshold = config.completionThreshold ?? DEFAULT_COMPLETION_THRESHOLD;
133
+ this.maxPositionReached = 0;
134
+
135
+ // Check if already completed (from previous session)
136
+ this.state.isCompleted = this._isContextCompleted(contextId);
137
+
138
+ // Check for saved position
139
+ const savedPosition = this._getSavedPosition(contextId);
140
+
141
+ // Set up event listeners
142
+ this._setupVideoListeners(video, contextId, savedPosition);
143
+
144
+ // Emit loadStart event
145
+ eventBus.emit('video:loadStart', {
146
+ contextId,
147
+ contextType: this.state.contextType,
148
+ src: config.src
149
+ });
150
+
151
+ logger.debug(`[VideoManager] Attached video: ${contextId}`);
152
+ }
153
+
154
+ /**
155
+ * Sets up event listeners on a video element.
156
+ * @private
157
+ * @param {HTMLVideoElement} video - The video element
158
+ * @param {string} contextId - The context identifier
159
+ * @param {number} savedPosition - Saved position to restore
160
+ */
161
+ _setupVideoListeners(video, contextId, savedPosition) {
162
+ // Store reference for cleanup
163
+ video._videoManagerContextId = contextId;
164
+ video._videoManagerListeners = {};
165
+
166
+ const listeners = video._videoManagerListeners;
167
+
168
+ listeners.loadedmetadata = () => {
169
+ if (isFinite(video.duration) && video.duration > 0) {
170
+ this.state.duration = video.duration;
171
+ }
172
+
173
+ // Restore position if we have one saved
174
+ if (savedPosition > 0 && savedPosition < video.duration) {
175
+ video.currentTime = savedPosition;
176
+ this.state.position = savedPosition;
177
+ this.maxPositionReached = savedPosition;
178
+ }
179
+
180
+ eventBus.emit('video:loaded', {
181
+ src: this.state.currentSrc,
182
+ duration: this.state.duration,
183
+ contextId,
184
+ contextType: this.state.contextType
185
+ });
186
+ };
187
+
188
+ listeners.play = () => {
189
+ this.state.isPlaying = true;
190
+ this._startPositionUpdates(video);
191
+ eventBus.emit('video:play', {
192
+ contextId,
193
+ contextType: this.state.contextType
194
+ });
195
+ };
196
+
197
+ listeners.pause = () => {
198
+ this.state.isPlaying = false;
199
+ this._stopPositionUpdates();
200
+ this._savePosition();
201
+ eventBus.emit('video:pause', {
202
+ contextId,
203
+ contextType: this.state.contextType,
204
+ position: this.state.position
205
+ });
206
+ };
207
+
208
+ listeners.ended = () => {
209
+ this.state.isPlaying = false;
210
+ this.state.position = video.duration;
211
+ this._stopPositionUpdates();
212
+
213
+ // Mark as completed when video ends
214
+ this._checkAndMarkCompleted();
215
+
216
+ eventBus.emit('video:ended', { contextId });
217
+ };
218
+
219
+ listeners.timeupdate = () => {
220
+ this.state.position = video.currentTime;
221
+
222
+ // Track max position for completion calculation
223
+ if (video.currentTime > this.maxPositionReached) {
224
+ this.maxPositionReached = video.currentTime;
225
+ }
226
+
227
+ // Check for completion threshold during playback
228
+ this._checkAndMarkCompleted();
229
+ };
230
+
231
+ listeners.volumechange = () => {
232
+ this.state.volume = video.volume;
233
+ this.state.isMuted = video.muted;
234
+ this._persistMuteState();
235
+ };
236
+
237
+ listeners.error = () => {
238
+ const error = video.error;
239
+ const errorMessage = error ? `${error.code}: ${error.message}` : 'Unknown error';
240
+
241
+ this.state.isPlaying = false;
242
+ this._stopPositionUpdates();
243
+
244
+ logger.error(`[VideoManager] Video playback error: ${errorMessage}`, { domain: 'video', operation: 'playback', src: this.state.currentSrc, contextId });
245
+ };
246
+
247
+ // Attach all listeners
248
+ for (const [event, handler] of Object.entries(listeners)) {
249
+ video.addEventListener(event, handler);
250
+ }
251
+
252
+ // Apply persisted mute state
253
+ video.muted = this.state.isMuted;
254
+ }
255
+
256
+ /**
257
+ * Detaches event listeners from a video element.
258
+ * @param {HTMLVideoElement} video - The video element
259
+ */
260
+ detachVideo(video) {
261
+ if (!video || !video._videoManagerListeners) return;
262
+
263
+ // Save position before detaching
264
+ if (video._videoManagerContextId === this.state.contextId) {
265
+ this._savePosition();
266
+ }
267
+
268
+ // Remove all listeners
269
+ for (const [event, handler] of Object.entries(video._videoManagerListeners)) {
270
+ video.removeEventListener(event, handler);
271
+ }
272
+
273
+ delete video._videoManagerListeners;
274
+ delete video._videoManagerContextId;
275
+
276
+ this._stopPositionUpdates();
277
+
278
+ // Clear state if this was the active video
279
+ if (video._videoManagerContextId === this.state.contextId) {
280
+ const wasMuted = this.state.isMuted;
281
+ this.state = {
282
+ currentSrc: null,
283
+ contextId: null,
284
+ contextType: 'standalone',
285
+ position: 0,
286
+ isPlaying: false,
287
+ isMuted: wasMuted,
288
+ duration: 0,
289
+ volume: this.state.volume,
290
+ required: false,
291
+ completionThreshold: DEFAULT_COMPLETION_THRESHOLD,
292
+ isCompleted: false
293
+ };
294
+ this.maxPositionReached = 0;
295
+ }
296
+
297
+ eventBus.emit('video:unloaded', { contextType: this.state.contextType });
298
+ logger.debug('[VideoManager] Detached video');
299
+ }
300
+
301
+ /**
302
+ * Gets the current video state.
303
+ * @returns {VideoState}
304
+ */
305
+ getState() {
306
+ return { ...this.state };
307
+ }
308
+
309
+ /**
310
+ * Gets the current playback position as a percentage.
311
+ * @returns {number} Percentage (0-100)
312
+ */
313
+ getProgressPercentage() {
314
+ if (!this.state.duration || !isFinite(this.state.duration)) return 0;
315
+ return (this.state.position / this.state.duration) * 100;
316
+ }
317
+
318
+ /**
319
+ * Checks if video is currently loaded.
320
+ * @returns {boolean}
321
+ */
322
+ hasVideo() {
323
+ return !!this.state.currentSrc;
324
+ }
325
+
326
+ /**
327
+ * Checks if the current video has been completed.
328
+ * @returns {boolean}
329
+ */
330
+ isCurrentVideoCompleted() {
331
+ return this.state.isCompleted;
332
+ }
333
+
334
+ /**
335
+ * Checks if video for a specific context has been completed.
336
+ * @param {string} contextId - The context identifier
337
+ * @returns {boolean}
338
+ */
339
+ isVideoCompleted(contextId) {
340
+ return this._isContextCompleted(contextId);
341
+ }
342
+
343
+ // =================================================================
344
+ // Private Methods
345
+ // =================================================================
346
+
347
+ /**
348
+ * Throws if not initialized.
349
+ * @private
350
+ */
351
+ _requireInitialized() {
352
+ if (!this.isInitialized) {
353
+ throw new Error('VideoManager: Not initialized. Call initialize() first.');
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Starts periodic position updates for UI.
359
+ * @private
360
+ * @param {HTMLVideoElement} video - The video element
361
+ */
362
+ _startPositionUpdates(video) {
363
+ this._stopPositionUpdates();
364
+ this.updateInterval = setInterval(() => {
365
+ if (!video || video.paused) {
366
+ this._stopPositionUpdates();
367
+ return;
368
+ }
369
+
370
+ let duration = this.state.duration;
371
+ if (!isFinite(duration) && isFinite(video.duration)) {
372
+ duration = video.duration;
373
+ this.state.duration = duration;
374
+ }
375
+
376
+ eventBus.emit('video:progress', {
377
+ position: this.state.position,
378
+ duration: duration,
379
+ percentage: this.getProgressPercentage()
380
+ });
381
+ }, 250);
382
+ }
383
+
384
+ /**
385
+ * Stops periodic position updates.
386
+ * @private
387
+ */
388
+ _stopPositionUpdates() {
389
+ if (this.updateInterval) {
390
+ clearInterval(this.updateInterval);
391
+ this.updateInterval = null;
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Checks if completion threshold reached and marks completed.
397
+ * @private
398
+ */
399
+ _checkAndMarkCompleted() {
400
+ if (this.state.isCompleted) return;
401
+ if (!this.state.duration || this.state.duration <= 0) return;
402
+
403
+ const completionPercentage = this.maxPositionReached / this.state.duration;
404
+
405
+ if (completionPercentage >= this.state.completionThreshold) {
406
+ this.state.isCompleted = true;
407
+ this._markContextCompleted(this.state.contextId);
408
+
409
+ logger.debug(`[VideoManager] Video completed: ${this.state.contextId}`);
410
+
411
+ eventBus.emit('video:complete', {
412
+ contextId: this.state.contextId,
413
+ contextType: this.state.contextType,
414
+ required: this.state.required
415
+ });
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Saves the current position for the current context.
421
+ * @private
422
+ */
423
+ _savePosition() {
424
+ if (!this.state.contextId || !this.state.position) return;
425
+
426
+ this.positionCache.set(this.state.contextId, this.state.position);
427
+ this._persistPositions();
428
+ }
429
+
430
+ /**
431
+ * Gets the saved position for a context.
432
+ * @private
433
+ * @param {string} contextId - Context identifier
434
+ * @returns {number} Saved position in seconds (0 if none)
435
+ */
436
+ _getSavedPosition(contextId) {
437
+ return this.positionCache.get(contextId) || 0;
438
+ }
439
+
440
+ /**
441
+ * Checks if a context has been completed.
442
+ * @private
443
+ * @param {string} contextId - Context identifier
444
+ * @returns {boolean}
445
+ */
446
+ _isContextCompleted(contextId) {
447
+ return this.completionCache.get(contextId) || false;
448
+ }
449
+
450
+ /**
451
+ * Marks a context as completed.
452
+ * @private
453
+ * @param {string} contextId - Context identifier
454
+ */
455
+ _markContextCompleted(contextId) {
456
+ if (!contextId) return;
457
+ this.completionCache.set(contextId, true);
458
+ this._persistCompletions();
459
+ }
460
+
461
+ /**
462
+ * Restores state from stateManager on initialization.
463
+ * @private
464
+ */
465
+ _hydrateFromState() {
466
+ try {
467
+ const videoState = stateManager.getDomainState('video');
468
+ if (videoState) {
469
+ // Restore position cache
470
+ if (videoState.positions) {
471
+ this.positionCache = new Map(Object.entries(videoState.positions));
472
+ }
473
+
474
+ // Restore completion cache
475
+ if (videoState.completions) {
476
+ this.completionCache = new Map(Object.entries(videoState.completions));
477
+ }
478
+
479
+ // Restore mute preference
480
+ if (typeof videoState.isMuted === 'boolean') {
481
+ this.state.isMuted = videoState.isMuted;
482
+ }
483
+
484
+ logger.debug('[VideoManager] Hydrated state from stateManager');
485
+ }
486
+ } catch (error) {
487
+ logger.warn('[VideoManager] Failed to hydrate state:', error.message);
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Persists position cache to stateManager.
493
+ * @private
494
+ */
495
+ _persistPositions() {
496
+ try {
497
+ const currentState = stateManager.getDomainState('video') || {};
498
+ currentState.positions = Object.fromEntries(this.positionCache);
499
+ stateManager.setDomainState('video', currentState);
500
+ } catch (error) {
501
+ logger.warn('[VideoManager] Failed to persist positions:', error.message);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Persists completion cache to stateManager.
507
+ * @private
508
+ */
509
+ _persistCompletions() {
510
+ try {
511
+ const currentState = stateManager.getDomainState('video') || {};
512
+ currentState.completions = Object.fromEntries(this.completionCache);
513
+ stateManager.setDomainState('video', currentState);
514
+ } catch (error) {
515
+ logger.warn('[VideoManager] Failed to persist completions:', error.message);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Persists mute state to stateManager.
521
+ * @private
522
+ */
523
+ _persistMuteState() {
524
+ try {
525
+ const currentState = stateManager.getDomainState('video') || {};
526
+ currentState.isMuted = this.state.isMuted;
527
+ stateManager.setDomainState('video', currentState);
528
+ } catch (error) {
529
+ logger.warn('[VideoManager] Failed to persist mute state:', error.message);
530
+ }
531
+ }
532
+ }
533
+
534
+ // Export singleton instance
535
+ const videoManager = new VideoManager();
536
+ export default videoManager;