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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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;
|