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,686 @@
1
+ /**
2
+ * @file video-player.js
3
+ * @description Video player UI component for course content.
4
+ *
5
+ * Standalone Usage:
6
+ * <div data-component="video-player"
7
+ * data-video-id="intro-video"
8
+ * data-video-src="video/intro.mp4"
9
+ * data-video-poster="images/intro-poster.jpg">
10
+ * </div>
11
+ *
12
+ * Engagement config for gating:
13
+ * - Standalone video: { type: 'videoComplete', videoId: 'intro-video', message: '...' }
14
+ *
15
+ * Controls:
16
+ * - Play/Pause toggle
17
+ * - Progress bar (clickable for seeking)
18
+ * - Current time / Duration display
19
+ * - Mute toggle
20
+ * - Fullscreen toggle
21
+ *
22
+ * @author Framework
23
+ * @version 1.0.0
24
+ */
25
+
26
+ export const schema = {
27
+ type: 'video-player',
28
+ description: 'Video player with custom controls and engagement tracking',
29
+ example: `<div data-component="video-player" data-video-id="demo-video" data-video-src="https://www.youtube.com/watch?v=dQw4w9WgXcQ">
30
+ <p style="color: #64748b; font-size: 0.875rem; font-style: italic;">🎬 Video player renders dynamically — supports native video, YouTube, and Vimeo.</p>
31
+ </div>`,
32
+ properties: {
33
+ videoId: { type: 'string', required: true, dataAttribute: 'data-video-id' },
34
+ videoSrc: { type: 'string', required: true, dataAttribute: 'data-video-src' },
35
+ poster: { type: 'string', dataAttribute: 'data-video-poster' },
36
+ autoplay: { type: 'boolean', default: false, dataAttribute: 'data-video-autoplay' }
37
+ },
38
+ structure: {
39
+ container: '[data-component="video-player"]',
40
+ children: {} // Content is dynamically rendered
41
+ }
42
+ };
43
+
44
+ export const metadata = {
45
+ category: 'ui-component',
46
+ cssFile: 'components/video-player.css',
47
+ engagementTracking: 'videoComplete',
48
+ emitsEvents: ['video:complete']
49
+ };
50
+
51
+ import { eventBus } from '../../core/event-bus.js';
52
+ import videoManager from '../../managers/video-manager.js';
53
+ import engagementManager from '../../engagement/engagement-manager.js';
54
+ import * as NavigationState from '../../navigation/NavigationState.js';
55
+ import { logger } from '../../utilities/logger.js';
56
+ import { iconManager } from '../../utilities/icons.js';
57
+
58
+ /** @type {Map<string, VideoPlayer>} Active player instances */
59
+ const playerInstances = new Map();
60
+
61
+ /**
62
+ * Formats seconds as MM:SS or H:MM:SS.
63
+ * @param {number} seconds
64
+ * @returns {string}
65
+ */
66
+ function formatTime(seconds) {
67
+ if (!seconds || !isFinite(seconds)) return '0:00';
68
+
69
+ const hrs = Math.floor(seconds / 3600);
70
+ const mins = Math.floor((seconds % 3600) / 60);
71
+ const secs = Math.floor(seconds % 60);
72
+
73
+ if (hrs > 0) {
74
+ return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
75
+ }
76
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
77
+ }
78
+
79
+ /**
80
+ * Detects if a URL is a YouTube video and extracts the video ID.
81
+ * Supports: youtube.com/watch?v=, youtu.be/, youtube.com/embed/
82
+ * @param {string} url
83
+ * @returns {{ type: 'youtube', id: string } | null}
84
+ */
85
+ function detectYouTube(url) {
86
+ if (!url) return null;
87
+
88
+ // youtube.com/watch?v=VIDEO_ID
89
+ let match = url.match(/(?:youtube\.com\/watch\?v=|youtube\.com\/watch\?.+&v=)([a-zA-Z0-9_-]{11})/);
90
+ if (match) return { type: 'youtube', id: match[1] };
91
+
92
+ // youtu.be/VIDEO_ID
93
+ match = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
94
+ if (match) return { type: 'youtube', id: match[1] };
95
+
96
+ // youtube.com/embed/VIDEO_ID
97
+ match = url.match(/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/);
98
+ if (match) return { type: 'youtube', id: match[1] };
99
+
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Detects if a URL is a Vimeo video and extracts the video ID.
105
+ * Supports: vimeo.com/VIDEO_ID, player.vimeo.com/video/VIDEO_ID
106
+ * @param {string} url
107
+ * @returns {{ type: 'vimeo', id: string } | null}
108
+ */
109
+ function detectVimeo(url) {
110
+ if (!url) return null;
111
+
112
+ // vimeo.com/VIDEO_ID or player.vimeo.com/video/VIDEO_ID
113
+ const match = url.match(/(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/);
114
+ if (match) return { type: 'vimeo', id: match[1] };
115
+
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Detects if URL is an external video platform and returns platform info.
121
+ * @param {string} url
122
+ * @returns {{ type: 'youtube' | 'vimeo', id: string } | null}
123
+ */
124
+ function detectExternalVideo(url) {
125
+ return detectYouTube(url) || detectVimeo(url);
126
+ }
127
+
128
+ /**
129
+ * Resolves video path relative to course/assets/.
130
+ * @param {string} src - Source path
131
+ * @returns {string} Resolved path
132
+ */
133
+ function resolvePath(src) {
134
+ if (src.startsWith('http') || src.startsWith('/') || src.startsWith('./')) {
135
+ return src;
136
+ }
137
+ return `./course/assets/${src}`;
138
+ }
139
+
140
+ /**
141
+ * Class representing a video player instance.
142
+ */
143
+ class VideoPlayer {
144
+ constructor(container) {
145
+ this.container = container;
146
+ this.videoId = container.dataset.videoId;
147
+ this.videoSrc = container.dataset.videoSrc;
148
+ this.posterSrc = container.dataset.videoPoster;
149
+ this.captionsSrc = container.dataset.videoCaptions;
150
+ this.required = container.dataset.videoRequired === 'true';
151
+ this.threshold = parseFloat(container.dataset.videoThreshold) || 0.95;
152
+ this.autoplay = container.dataset.videoAutoplay === 'true';
153
+
154
+ if (!this.videoId) {
155
+ throw new Error('[VideoPlayer] requires data-video-id');
156
+ }
157
+ if (!this.videoSrc) {
158
+ throw new Error(`[VideoPlayer] "${this.videoId}" requires data-video-src`);
159
+ }
160
+
161
+ // Detect external video platforms (YouTube, Vimeo)
162
+ this.externalVideo = detectExternalVideo(this.videoSrc);
163
+ this.isExternal = !!this.externalVideo;
164
+
165
+ this.contextId = `video-${this.videoId}`;
166
+ this.video = null; // Native video element (null for external)
167
+ this.iframe = null; // External video iframe
168
+ this.elements = {};
169
+ this.eventHandlers = {};
170
+ this.isFullscreen = false;
171
+
172
+ this._render();
173
+ this._cacheElements();
174
+ this._setupEventListeners();
175
+
176
+ // Only subscribe and attach for native videos
177
+ if (!this.isExternal) {
178
+ this._subscribeToVideoEvents();
179
+ this._attachToManager();
180
+ } else {
181
+ // For external videos, mark as loaded immediately
182
+ logger.debug(`[VideoPlayer] External video (${this.externalVideo.type}): ${this.videoId}`);
183
+ }
184
+
185
+ playerInstances.set(this.videoId, this);
186
+ logger.debug(`[VideoPlayer] Initialized: ${this.videoId}`);
187
+ }
188
+
189
+ _render() {
190
+ // External video: render responsive iframe with platform controls
191
+ if (this.isExternal) {
192
+ this._renderExternalVideo();
193
+ return;
194
+ }
195
+
196
+ // Native video: render HTML5 video with custom controls
197
+ this._renderNativeVideo();
198
+ }
199
+
200
+ _renderExternalVideo() {
201
+ const { type, id } = this.externalVideo;
202
+ let embedUrl = '';
203
+
204
+ if (type === 'youtube') {
205
+ // YouTube embed with enablejsapi for future API control
206
+ embedUrl = `https://www.youtube.com/embed/${id}?rel=0&modestbranding=1&enablejsapi=1`;
207
+ if (this.autoplay) embedUrl += '&autoplay=1';
208
+ } else if (type === 'vimeo') {
209
+ // Vimeo embed
210
+ embedUrl = `https://player.vimeo.com/video/${id}?dnt=1`;
211
+ if (this.autoplay) embedUrl += '&autoplay=1';
212
+ }
213
+
214
+ this.container.innerHTML = `
215
+ <div class="video-player-wrapper video-player-external video-player-${type}">
216
+ <div class="video-player-media video-player-responsive">
217
+ <iframe
218
+ class="video-player-iframe"
219
+ src="${embedUrl}"
220
+ frameborder="0"
221
+ allow="autoplay; fullscreen"
222
+ data-testid="video-${this.videoId}"
223
+ ></iframe>
224
+ </div>
225
+ </div>
226
+ `;
227
+ }
228
+
229
+ _renderNativeVideo() {
230
+ const resolvedSrc = resolvePath(this.videoSrc);
231
+ const resolvedPoster = this.posterSrc ? resolvePath(this.posterSrc) : '';
232
+ const resolvedCaptions = this.captionsSrc ? resolvePath(this.captionsSrc) : '';
233
+
234
+ this.container.innerHTML = `
235
+ <div class="video-player-wrapper">
236
+ <div class="video-player-media">
237
+ <video
238
+ class="video-player-element"
239
+ preload="metadata"
240
+ playsinline
241
+ ${resolvedPoster ? `poster="${resolvedPoster}"` : ''}
242
+ data-testid="video-${this.videoId}"
243
+ >
244
+ <source src="${resolvedSrc}" type="video/mp4">
245
+ ${resolvedCaptions ? `<track kind="captions" src="${resolvedCaptions}" srclang="en" label="English">` : ''}
246
+ Your browser does not support the video element.
247
+ </video>
248
+ <div class="video-player-overlay" data-action="video-play-pause">
249
+ <button type="button" class="video-overlay-play-btn" aria-label="Play video">
250
+ ${iconManager.getIcon('play', { size: 'xl' })}
251
+ </button>
252
+ </div>
253
+ </div>
254
+ <div class="video-player-controls" role="group" aria-label="Video controls">
255
+ <button type="button" class="video-btn video-btn-play" aria-label="Play video"
256
+ data-action="video-play-pause" data-testid="video-play-${this.videoId}">
257
+ <span class="video-icon video-icon-play" aria-hidden="true">${iconManager.getIcon('play')}</span>
258
+ <span class="video-icon video-icon-pause" aria-hidden="true" style="display:none;">${iconManager.getIcon('pause')}</span>
259
+ </button>
260
+ <div class="video-progress-container" role="slider" aria-label="Video progress"
261
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" tabindex="0"
262
+ data-action="video-seek" data-testid="video-progress-${this.videoId}">
263
+ <div class="video-progress-track">
264
+ <div class="video-progress-fill"></div>
265
+ <div class="video-progress-handle"></div>
266
+ </div>
267
+ </div>
268
+ <span class="video-time" aria-live="off" data-testid="video-time-${this.videoId}">
269
+ <span class="video-time-current">0:00</span>
270
+ <span class="video-time-separator">/</span>
271
+ <span class="video-time-duration">0:00</span>
272
+ </span>
273
+ <button type="button" class="video-btn video-btn-mute" aria-label="Mute video"
274
+ data-action="video-mute" data-testid="video-mute-${this.videoId}">
275
+ <span class="video-icon video-icon-unmuted" aria-hidden="true">${iconManager.getIcon('volume-2')}</span>
276
+ <span class="video-icon video-icon-muted" aria-hidden="true" style="display:none;">${iconManager.getIcon('volume-x')}</span>
277
+ </button>
278
+ <button type="button" class="video-btn video-btn-fullscreen" aria-label="Enter fullscreen"
279
+ data-action="video-fullscreen" data-testid="video-fullscreen-${this.videoId}">
280
+ <span class="video-icon video-icon-expand" aria-hidden="true">${iconManager.getIcon('maximize')}</span>
281
+ <span class="video-icon video-icon-compress" aria-hidden="true" style="display:none;">${iconManager.getIcon('minimize')}</span>
282
+ </button>
283
+ </div>
284
+ </div>
285
+ `;
286
+ }
287
+
288
+ _cacheElements() {
289
+ // For external videos, cache iframe instead of video
290
+ if (this.isExternal) {
291
+ this.iframe = this.container.querySelector('iframe');
292
+ this.elements.wrapper = this.container.querySelector('.video-player-wrapper');
293
+ return;
294
+ }
295
+
296
+ this.video = this.container.querySelector('video');
297
+ this.elements.wrapper = this.container.querySelector('.video-player-wrapper');
298
+ this.elements.overlay = this.container.querySelector('.video-player-overlay');
299
+ this.elements.overlayPlayBtn = this.container.querySelector('.video-overlay-play-btn');
300
+ this.elements.playPauseBtn = this.container.querySelector('[data-action="video-play-pause"].video-btn');
301
+ this.elements.muteBtn = this.container.querySelector('[data-action="video-mute"]');
302
+ this.elements.fullscreenBtn = this.container.querySelector('[data-action="video-fullscreen"]');
303
+ this.elements.progressBar = this.container.querySelector('.video-progress-container');
304
+ this.elements.progressFill = this.container.querySelector('.video-progress-fill');
305
+ this.elements.progressHandle = this.container.querySelector('.video-progress-handle');
306
+ this.elements.timeCurrent = this.container.querySelector('.video-time-current');
307
+ this.elements.timeDuration = this.container.querySelector('.video-time-duration');
308
+ }
309
+
310
+ _setupEventListeners() {
311
+ // External videos use platform's native controls
312
+ if (this.isExternal) {
313
+ // Only fullscreen detection for wrapper
314
+ document.addEventListener('fullscreenchange', this._handleFullscreenChange.bind(this));
315
+ document.addEventListener('webkitfullscreenchange', this._handleFullscreenChange.bind(this));
316
+ return;
317
+ }
318
+
319
+ // Control bar click delegation
320
+ this.container.addEventListener('click', this._handleClick.bind(this));
321
+
322
+ // Progress bar interactions
323
+ if (this.elements.progressBar) {
324
+ this.elements.progressBar.addEventListener('keydown', this._handleProgressKeydown.bind(this));
325
+ this.elements.progressBar.addEventListener('mousedown', this._handleProgressMousedown.bind(this));
326
+ }
327
+
328
+ // Fullscreen change detection
329
+ document.addEventListener('fullscreenchange', this._handleFullscreenChange.bind(this));
330
+ document.addEventListener('webkitfullscreenchange', this._handleFullscreenChange.bind(this));
331
+
332
+ // Double-click video to toggle fullscreen
333
+ if (this.video) {
334
+ this.video.addEventListener('dblclick', () => this._toggleFullscreen());
335
+ }
336
+
337
+ // Click overlay to play
338
+ if (this.elements.overlay) {
339
+ this.elements.overlay.addEventListener('click', (e) => {
340
+ e.stopPropagation();
341
+ this._togglePlayPause();
342
+ });
343
+ }
344
+ }
345
+
346
+ _handleClick(event) {
347
+ const target = event.target.closest('[data-action]');
348
+ if (!target) return;
349
+
350
+ const action = target.dataset.action;
351
+
352
+ switch (action) {
353
+ case 'video-play-pause':
354
+ this._togglePlayPause();
355
+ break;
356
+ case 'video-mute':
357
+ this._toggleMute();
358
+ break;
359
+ case 'video-fullscreen':
360
+ this._toggleFullscreen();
361
+ break;
362
+ }
363
+ }
364
+
365
+ _togglePlayPause() {
366
+ if (!this.video) return;
367
+
368
+ if (this.video.paused) {
369
+ this.video.play().catch(err => {
370
+ logger.warn(`[VideoPlayer] Play failed for ${this.videoId}:`, err.message);
371
+ });
372
+ } else {
373
+ this.video.pause();
374
+ }
375
+ }
376
+
377
+ _toggleMute() {
378
+ if (!this.video) return;
379
+ this.video.muted = !this.video.muted;
380
+ }
381
+
382
+ _toggleFullscreen() {
383
+ const wrapper = this.elements.wrapper;
384
+ if (!wrapper) return;
385
+
386
+ if (!document.fullscreenElement && !document.webkitFullscreenElement) {
387
+ if (wrapper.requestFullscreen) {
388
+ wrapper.requestFullscreen();
389
+ } else if (wrapper.webkitRequestFullscreen) {
390
+ wrapper.webkitRequestFullscreen();
391
+ }
392
+ } else {
393
+ if (document.exitFullscreen) {
394
+ document.exitFullscreen();
395
+ } else if (document.webkitExitFullscreen) {
396
+ document.webkitExitFullscreen();
397
+ }
398
+ }
399
+ }
400
+
401
+ _handleFullscreenChange() {
402
+ const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
403
+ this.isFullscreen = isFullscreen;
404
+
405
+ if (this.elements.wrapper) {
406
+ this.elements.wrapper.classList.toggle('video-player-fullscreen', isFullscreen);
407
+ }
408
+
409
+ // Update fullscreen button icon
410
+ const btn = this.elements.fullscreenBtn;
411
+ if (btn) {
412
+ const expandIcon = btn.querySelector('.video-icon-expand');
413
+ const compressIcon = btn.querySelector('.video-icon-compress');
414
+ if (isFullscreen) {
415
+ if (expandIcon) expandIcon.style.display = 'none';
416
+ if (compressIcon) compressIcon.style.display = '';
417
+ btn.setAttribute('aria-label', 'Exit fullscreen');
418
+ } else {
419
+ if (expandIcon) expandIcon.style.display = '';
420
+ if (compressIcon) compressIcon.style.display = 'none';
421
+ btn.setAttribute('aria-label', 'Enter fullscreen');
422
+ }
423
+ }
424
+ }
425
+
426
+ _handleProgressKeydown(event) {
427
+ if (!this.video || !this.video.duration) return;
428
+
429
+ let seekDelta = 0;
430
+ switch (event.key) {
431
+ case 'ArrowLeft': seekDelta = -5; break;
432
+ case 'ArrowRight': seekDelta = 5; break;
433
+ case 'Home':
434
+ this.video.currentTime = 0;
435
+ event.preventDefault();
436
+ return;
437
+ case 'End':
438
+ this.video.currentTime = this.video.duration;
439
+ event.preventDefault();
440
+ return;
441
+ default: return;
442
+ }
443
+ event.preventDefault();
444
+ this.video.currentTime = Math.max(0, Math.min(this.video.currentTime + seekDelta, this.video.duration));
445
+ }
446
+
447
+ _handleProgressMousedown(event) {
448
+ if (!this.video || !this.video.duration) return;
449
+
450
+ const progressBar = this.elements.progressBar;
451
+ const rect = progressBar.getBoundingClientRect();
452
+
453
+ const seek = (clientX) => {
454
+ const pct = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
455
+ this.video.currentTime = (pct / 100) * this.video.duration;
456
+ };
457
+
458
+ seek(event.clientX);
459
+
460
+ const onMouseMove = (e) => seek(e.clientX);
461
+ const onMouseUp = () => {
462
+ document.removeEventListener('mousemove', onMouseMove);
463
+ document.removeEventListener('mouseup', onMouseUp);
464
+ progressBar.classList.remove('dragging');
465
+ };
466
+
467
+ progressBar.classList.add('dragging');
468
+ document.addEventListener('mousemove', onMouseMove);
469
+ document.addEventListener('mouseup', onMouseUp);
470
+ }
471
+
472
+ _attachToManager() {
473
+ videoManager.attachVideo(this.video, this.contextId, {
474
+ src: this.videoSrc,
475
+ contextType: 'standalone',
476
+ required: this.required,
477
+ completionThreshold: this.threshold
478
+ });
479
+
480
+ // Autoplay if configured
481
+ if (this.autoplay) {
482
+ this.video.play().catch(err => {
483
+ logger.warn(`[VideoPlayer] Autoplay blocked for ${this.videoId}:`, err.message);
484
+ });
485
+ }
486
+ }
487
+
488
+ _subscribeToVideoEvents() {
489
+ this.eventHandlers.loaded = ({ contextId, duration }) => {
490
+ if (contextId === this.contextId) {
491
+ this._updateDuration(duration);
492
+ // Sync mute state
493
+ this._setMutedState(videoManager.getState().isMuted);
494
+ }
495
+ };
496
+
497
+ this.eventHandlers.play = ({ contextId }) => {
498
+ if (contextId === this.contextId) {
499
+ this._setPlayingState(true);
500
+ this._hideOverlay();
501
+ }
502
+ };
503
+
504
+ this.eventHandlers.pause = ({ contextId }) => {
505
+ if (contextId === this.contextId) {
506
+ this._setPlayingState(false);
507
+ this._showOverlay();
508
+ }
509
+ };
510
+
511
+ this.eventHandlers.ended = ({ contextId }) => {
512
+ if (contextId === this.contextId) {
513
+ this._setPlayingState(false);
514
+ this._showOverlay();
515
+ }
516
+ };
517
+
518
+ this.eventHandlers.progress = ({ position, duration }) => {
519
+ if (videoManager.getState().contextId === this.contextId) {
520
+ this._updateProgress(position, duration);
521
+ }
522
+ };
523
+
524
+ this.eventHandlers.complete = ({ contextId }) => {
525
+ if (contextId === this.contextId && this.required) {
526
+ const currentSlideId = NavigationState.getCurrentSlideId();
527
+ if (currentSlideId) {
528
+ engagementManager.trackStandaloneVideoComplete(currentSlideId, this.videoId);
529
+ }
530
+ }
531
+ };
532
+
533
+ eventBus.on('video:loaded', this.eventHandlers.loaded);
534
+ eventBus.on('video:play', this.eventHandlers.play);
535
+ eventBus.on('video:pause', this.eventHandlers.pause);
536
+ eventBus.on('video:ended', this.eventHandlers.ended);
537
+ eventBus.on('video:progress', this.eventHandlers.progress);
538
+ eventBus.on('video:complete', this.eventHandlers.complete);
539
+ }
540
+
541
+ _showOverlay() {
542
+ if (this.elements.overlay) {
543
+ this.elements.overlay.classList.remove('hidden');
544
+ }
545
+ }
546
+
547
+ _hideOverlay() {
548
+ if (this.elements.overlay) {
549
+ this.elements.overlay.classList.add('hidden');
550
+ }
551
+ }
552
+
553
+ _setPlayingState(isPlaying) {
554
+ const btn = this.elements.playPauseBtn;
555
+ if (!btn) return;
556
+
557
+ const playIcon = btn.querySelector('.video-icon-play');
558
+ const pauseIcon = btn.querySelector('.video-icon-pause');
559
+
560
+ if (isPlaying) {
561
+ if (playIcon) playIcon.style.display = 'none';
562
+ if (pauseIcon) pauseIcon.style.display = '';
563
+ btn.setAttribute('aria-label', 'Pause video');
564
+ btn.classList.add('playing');
565
+ } else {
566
+ if (playIcon) playIcon.style.display = '';
567
+ if (pauseIcon) pauseIcon.style.display = 'none';
568
+ btn.setAttribute('aria-label', 'Play video');
569
+ btn.classList.remove('playing');
570
+ }
571
+ }
572
+
573
+ _setMutedState(isMuted) {
574
+ const btn = this.elements.muteBtn;
575
+ if (!btn) return;
576
+
577
+ const unmutedIcon = btn.querySelector('.video-icon-unmuted');
578
+ const mutedIcon = btn.querySelector('.video-icon-muted');
579
+
580
+ if (isMuted) {
581
+ if (unmutedIcon) unmutedIcon.style.display = 'none';
582
+ if (mutedIcon) mutedIcon.style.display = '';
583
+ btn.setAttribute('aria-label', 'Unmute video');
584
+ btn.classList.add('muted');
585
+ } else {
586
+ if (unmutedIcon) unmutedIcon.style.display = '';
587
+ if (mutedIcon) mutedIcon.style.display = 'none';
588
+ btn.setAttribute('aria-label', 'Mute video');
589
+ btn.classList.remove('muted');
590
+ }
591
+ }
592
+
593
+ _updateProgress(position, duration) {
594
+ const pct = duration > 0 ? (position / duration) * 100 : 0;
595
+ if (this.elements.progressFill) this.elements.progressFill.style.width = `${pct}%`;
596
+ if (this.elements.progressHandle) this.elements.progressHandle.style.left = `${pct}%`;
597
+ if (this.elements.progressBar) this.elements.progressBar.setAttribute('aria-valuenow', Math.round(pct));
598
+ if (this.elements.timeCurrent) this.elements.timeCurrent.textContent = formatTime(position);
599
+ }
600
+
601
+ _updateDuration(duration) {
602
+ if (this.elements.timeDuration) {
603
+ this.elements.timeDuration.textContent = formatTime(duration);
604
+ }
605
+ }
606
+
607
+ destroy() {
608
+ // Unsubscribe from events
609
+ eventBus.off('video:loaded', this.eventHandlers.loaded);
610
+ eventBus.off('video:play', this.eventHandlers.play);
611
+ eventBus.off('video:pause', this.eventHandlers.pause);
612
+ eventBus.off('video:ended', this.eventHandlers.ended);
613
+ eventBus.off('video:progress', this.eventHandlers.progress);
614
+ eventBus.off('video:complete', this.eventHandlers.complete);
615
+
616
+ // Detach from manager
617
+ if (this.video) {
618
+ videoManager.detachVideo(this.video);
619
+ }
620
+
621
+ // Remove fullscreen listeners
622
+ document.removeEventListener('fullscreenchange', this._handleFullscreenChange);
623
+ document.removeEventListener('webkitfullscreenchange', this._handleFullscreenChange);
624
+
625
+ playerInstances.delete(this.videoId);
626
+ logger.debug(`[VideoPlayer] Destroyed: ${this.videoId}`);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Initializes a single video player element.
632
+ * @param {HTMLElement} element - The video player container element
633
+ * @returns {VideoPlayer|null} The initialized player or null on error
634
+ */
635
+ export function init(element) {
636
+ try {
637
+ return new VideoPlayer(element);
638
+ } catch (error) {
639
+ logger.error('[VideoPlayer] Init failed:', error.message);
640
+ return null;
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Initializes all video players in a container.
646
+ * @param {HTMLElement} root - The root element to scan
647
+ * @returns {VideoPlayer[]} Array of initialized players
648
+ */
649
+ export function initVideoPlayers(root) {
650
+ const containers = root.querySelectorAll('[data-component="video-player"]');
651
+ const players = [];
652
+
653
+ containers.forEach(container => {
654
+ const player = init(container);
655
+ if (player) {
656
+ players.push(player);
657
+ }
658
+ });
659
+
660
+ return players;
661
+ }
662
+
663
+ /**
664
+ * Destroys all active video player instances.
665
+ */
666
+ export function destroyAllVideoPlayers() {
667
+ playerInstances.forEach(player => player.destroy());
668
+ playerInstances.clear();
669
+ }
670
+
671
+ /**
672
+ * Gets an active player by video ID.
673
+ * @param {string} videoId
674
+ * @returns {VideoPlayer|undefined}
675
+ */
676
+ export function getVideoPlayer(videoId) {
677
+ return playerInstances.get(videoId);
678
+ }
679
+
680
+ /**
681
+ * Gets all active video IDs on the current slide.
682
+ * @returns {string[]}
683
+ */
684
+ export function getActiveVideoIds() {
685
+ return Array.from(playerInstances.keys());
686
+ }