@interactive-video-labs/core
Version:
Framework-agnostic interactive video engine core
919 lines (896 loc) • 28.8 kB
JavaScript
/**
*
* Framework-agnostic interactive video engine core
* @interactive-video-labs/core v0.2.0
* @author Taj
* @license MIT
*
*/
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Analytics: () => Analytics,
CueHandler: () => CueHandler,
IVLabsPlayer: () => IVLabsPlayer,
InteractionManager: () => InteractionManager,
StateMachine: () => StateMachine
});
module.exports = __toCommonJS(index_exports);
// src/stateMachine.ts
var StateMachine = class {
/**
* Creates an instance of StateMachine.
* @param initialState - The initial state of the state machine.
*/
constructor(initialState) {
this.transitions = [];
this.currentState = initialState;
}
/**
* Gets the current state of the state machine.
* @returns The current state.
*/
getState() {
return this.currentState;
}
/**
* Adds a new state transition to the state machine.
* @param transition - The state transition to be added.
*/
addTransition(transition) {
this.transitions.push(transition);
}
/**
* Transitions to a new state if the transition is valid.
* @param targetState - The state to transition to.
* @returns True if the transition was successful, false otherwise.
*/
transitionTo(targetState) {
const valid = this.transitions.find(
(t) => t.from === this.currentState && t.to === targetState && (!t.condition || t.condition())
);
if (valid) {
valid.action?.();
this.currentState = targetState;
return true;
}
return false;
}
/**
* Resets the state machine to its initial state.
* @param initialState - Optional initial state to reset to.
*/
reset(initialState) {
this.currentState = initialState || this.transitions[0]?.from || this.currentState;
}
};
// src/cueHandler.ts
var CueHandler = class {
/**
* Creates an instance of CueHandler.
* @param video - The HTMLVideoElement to monitor for cue points.
*/
constructor(video) {
this.cues = [];
this.triggered = /* @__PURE__ */ new Set();
this.videoElement = video;
this.handleTimeUpdate = this.handleTimeUpdate.bind(this);
}
/**
* Registers cue points and sets up the video element to listen for time updates.
* @param cues - An array of Cue objects to register.
*/
registerCues(cues) {
this.cues = cues.sort((a, b) => a.time - b.time);
this.triggered.clear();
}
loadCues(cues) {
this.registerCues(cues);
this.videoElement.addEventListener("timeupdate", this.handleTimeUpdate);
}
/**
* Sets the callback function to be called when a cue point is reached.
* @param callback - The callback function to be executed.
*/
onCue(callback) {
this.callback = callback;
}
/**
* Starts listening for cue points.
*/
start() {
this.videoElement.addEventListener("timeupdate", this.handleTimeUpdate);
}
/**
* Stops listening for cue points.
*/
stop() {
this.videoElement.removeEventListener("timeupdate", this.handleTimeUpdate);
}
/**
* Handles the time update event from the video element.
* Checks if the current time matches any registered cue points and triggers the callback.
*/
handleTimeUpdate() {
const currentTime = this.videoElement.currentTime;
for (const cue of this.cues) {
if (!this.triggered.has(cue.time) && currentTime >= cue.time) {
this.triggered.add(cue.time);
this.callback?.(cue);
}
}
}
/**
* Destroys the CueHandler instance.
*/
destroy() {
this.triggered.clear();
}
};
// src/interactionManager.ts
var InteractionManager = class {
constructor(container, i18n, decisionAdapter) {
this.interactionDiv = null;
this.interactionRenderers = {
choice: this.renderChoiceInteraction.bind(this),
text: this.renderTextInteraction.bind(this),
rating: this.renderRatingInteraction.bind(this),
"choice-video-segment-change": this.renderChoiceVideoSegmentChangeInteraction.bind(this),
default: this.renderDefaultInteraction.bind(this)
};
this.container = container;
this.i18n = i18n;
this.decisionAdapter = decisionAdapter;
this.interactionStore = /* @__PURE__ */ new Map();
}
loadInteractions(interactions) {
interactions.forEach((cue) => {
if (cue.time != null && cue.payload?.interaction) {
this.interactionStore.set(cue.time, cue);
}
});
}
onPrompt(handler) {
this.onPromptCallback = handler;
}
onResponse(handler) {
this.onResponseCallback = handler;
}
handleInteractionCue(cue) {
const payload = cue.payload?.interaction;
if (payload) {
this.onPromptCallback?.(payload, cue);
this.renderInteraction(payload, cue);
}
}
renderInteraction(payload, cue) {
this.clearInteractions();
this.interactionDiv = document.createElement("div");
this.interactionDiv.className = "ivl-interaction-overlay";
if (payload.title) {
const title = document.createElement("h3");
title.textContent = this.i18n.translate(payload.title);
this.interactionDiv.appendChild(title);
}
if (payload.description) {
const desc = document.createElement("p");
desc.textContent = this.i18n.translate(payload.description);
this.interactionDiv.appendChild(desc);
}
const renderer = this.interactionRenderers[payload.type] || this.interactionRenderers.default;
renderer(payload, cue);
this.container.appendChild(this.interactionDiv);
}
renderRatingInteraction(payload, cue) {
if (!this.interactionDiv) return;
const question = document.createElement("p");
question.textContent = this.i18n.translate(payload.question);
this.interactionDiv.appendChild(question);
const ratingContainer = document.createElement("div");
ratingContainer.className = "ivl-rating-container";
for (let i = 1; i <= 5; i++) {
const button = document.createElement("button");
button.className = "ivl-rating-button";
button.dataset.response = String(i);
button.textContent = `${i} \u2605`;
ratingContainer.appendChild(button);
}
this.interactionDiv.appendChild(ratingContainer);
ratingContainer.addEventListener("click", (event) => {
const target = event.target;
if (target.matches(".ivl-rating-button")) {
this.handleUserResponse(target.dataset.response, cue);
this.clearInteractions();
}
});
}
renderChoiceInteraction(payload, cue) {
if (!this.interactionDiv) return;
const question = document.createElement("p");
question.textContent = this.i18n.translate(payload.question);
this.interactionDiv.appendChild(question);
const buttonContainer = document.createElement("div");
buttonContainer.className = "ivl-choice-buttons";
payload.options.forEach((option) => {
const button = document.createElement("button");
button.className = "ivl-choice-button";
button.dataset.response = option;
button.textContent = this.i18n.translate(option);
buttonContainer.appendChild(button);
});
this.interactionDiv.appendChild(buttonContainer);
buttonContainer.addEventListener("click", (event) => {
const target = event.target;
if (target.matches(".ivl-choice-button")) {
this.handleUserResponse(target.dataset.response, cue);
this.clearInteractions();
}
});
}
renderChoiceVideoSegmentChangeInteraction(payload, cue) {
if (!this.interactionDiv) return;
const question = document.createElement("p");
question.textContent = this.i18n.translate(payload.question);
this.interactionDiv.appendChild(question);
const buttonContainer = document.createElement("div");
buttonContainer.className = "ivl-choice-buttons";
payload.options.forEach((option) => {
const button = document.createElement("button");
button.className = "ivl-choice-button";
button.dataset.response = option.video;
button.textContent = this.i18n.translate(option.level);
buttonContainer.appendChild(button);
});
this.interactionDiv.appendChild(buttonContainer);
buttonContainer.addEventListener("click", (event) => {
const target = event.target;
if (target.matches(".ivl-choice-button")) {
this.handleUserResponse(target.dataset.response, cue);
this.clearInteractions();
}
});
}
renderTextInteraction(payload, cue) {
if (!this.interactionDiv) return;
const question = document.createElement("p");
question.textContent = this.i18n.translate(payload.question);
this.interactionDiv.appendChild(question);
const input = document.createElement("input");
input.type = "text";
input.placeholder = this.i18n.translate("Enter your response");
this.interactionDiv.appendChild(input);
const button = document.createElement("button");
button.textContent = this.i18n.translate("Submit");
this.interactionDiv.appendChild(button);
button.addEventListener("click", () => {
this.handleUserResponse(input.value, cue);
this.clearInteractions();
});
}
renderDefaultInteraction(payload, cue) {
if (!this.interactionDiv) return;
const question = document.createElement("p");
question.textContent = this.i18n.translate(payload.question);
this.interactionDiv.appendChild(question);
const button = document.createElement("button");
button.textContent = this.i18n.translate("Respond");
this.interactionDiv.appendChild(button);
button.addEventListener("click", () => {
this.handleUserResponse("User responded!", cue);
this.clearInteractions();
});
}
clearInteractions() {
this.interactionDiv?.remove();
this.interactionDiv = null;
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
handleUserResponse(response, cue) {
const payload = cue.payload?.interaction;
if (!this.onResponseCallback) return;
const decision = {
cueId: cue.id,
choice: response,
timestamp: Date.now(),
metadata: { interactionType: payload?.type }
};
this.decisionAdapter.saveDecision(decision);
if (payload?.type === "choice-video-segment-change") {
this.onResponseCallback({ nextSegment: response }, cue);
} else if (payload && typeof payload.response === "object" && payload.response[response]) {
this.onResponseCallback(payload.response[response], cue);
} else {
this.onResponseCallback(response, cue);
}
}
destroy() {
this.onPromptCallback = void 0;
this.onResponseCallback = void 0;
this.clearInteractions();
this.interactionStore.clear();
}
};
// src/analytics.ts
var Analytics = class {
/**
* Initializes the Analytics instance.
* @param options - Optional configuration for the analytics instance.
*/
constructor(options = {}) {
this.options = options;
this.hooks = {};
if (this.options.debug) {
console.log("[Analytics] Initialized with options:", this.options);
}
}
/**
* Registers a new analytics hook for a specific event.
* @param event - The event to listen for.
* @param hook - The function to be called when the event is tracked.
*/
on(event, hook) {
if (!this.hooks[event]) {
this.hooks[event] = [];
}
this.hooks[event].push(hook);
}
/**
* Tracks an event with optional payload.
* @param event - The event to be tracked.
* @param payload - Optional data associated with the event.
*/
track(event, payload) {
if (this.hooks[event]) {
this.hooks[event].forEach((hook) => hook(event, payload));
}
}
/**
* Resets the analytics hooks.
*/
reset() {
this.hooks = {};
}
};
// src/segmentManager.ts
var SegmentManager = class {
constructor(videoElement) {
this._mainVideoSrc = null;
this._mainVideoCurrentTime = 0;
this.videoElement = videoElement;
this.bindEvents();
}
bindEvents() {
this.videoElement.addEventListener("ended", () => {
if (this._mainVideoSrc) {
console.log("Segment ended. Resuming main video:", this._mainVideoSrc);
const resumeSrc = this._mainVideoSrc;
const resumeTime = this._mainVideoCurrentTime;
this._mainVideoSrc = null;
this._mainVideoCurrentTime = 0;
this.videoElement.src = resumeSrc;
this.videoElement.load();
this.videoElement.addEventListener(
"loadedmetadata",
() => {
this.videoElement.currentTime = resumeTime;
this.videoElement.play().catch((err) => {
console.error("Error resuming main video:", err);
});
},
{ once: true }
);
}
});
}
/**
* Handles the transition to a new video segment.
* Saves the current main video state and plays the new segment.
* @param newSegmentUrl The URL of the new video segment to play.
*/
playSegment(newSegmentUrl) {
this._mainVideoSrc = this.videoElement.src;
this._mainVideoCurrentTime = this.videoElement.currentTime;
this.videoElement.src = newSegmentUrl;
this.videoElement.load();
this.videoElement.addEventListener(
"loadedmetadata",
() => {
this.videoElement.play().catch((error) => {
console.error("Error playing segment video:", error);
});
console.log("Playing segment video:", newSegmentUrl);
},
{ once: true }
);
}
/**
* Destroys the SegmentChangeManager instance.
* Removes event listeners.
*/
destroy() {
}
};
// src/i18n.ts
var I18n = class {
constructor() {
this.translations = {};
this.locale = "en";
}
/**
* Loads translations for a specific locale.
* @param locale - The locale to load translations for.
* @param translations - The translation data.
*/
load(locale, translations) {
this.translations[locale] = translations;
}
/**
* Sets the current locale for localization.
* @param locale - The new locale.
*/
setLocale(locale) {
this.locale = locale;
}
/**
* Translates a key to the current locale.
* @param key - The key to translate.
* @param options - Optional placeholders to replace in the translation.
* @returns The translated string or the original key if not found.
*/
translate(key, options) {
const translation = this.getTranslation(key, this.locale) || this.getTranslation(key, "en");
if (!translation) {
return key;
}
if (typeof translation === "string") {
return this.replacePlaceholders(translation, options);
}
return key;
}
/**
* Gets the translation for a key in a specific locale.
* @param key - The key to translate.
* @param locale - The locale to get the translation for.
* @returns The translated string or undefined if not found.
*/
getTranslation(key, locale) {
const keys = key.split(".");
let current = this.translations[locale];
for (const k of keys) {
if (current && typeof current === "object" && k in current) {
current = current[k];
} else {
return void 0;
}
}
return typeof current === "string" ? current : void 0;
}
/**
* Replaces placeholders in a translation string.
* @param text - The translation string with placeholders.
* @param options - Optional placeholders to replace in the translation.
* @returns The translated string with placeholders replaced.
*/
replacePlaceholders(text, options) {
if (!options) {
return text;
}
return Object.entries(options).reduce((acc, [key, value]) => {
return acc.replace(`{{${key}}}`, value);
}, text);
}
};
// src/InMemoryDecisionAdapter.ts
var InMemoryDecisionAdapter = class {
constructor() {
this.decisions = [];
}
/**
* Saves a decision to the in-memory store.
* @param decision - The decision to save.
*/
async saveDecision(decision) {
this.decisions.push(decision);
}
/**
* Retrieves the decision history from the in-memory store.
* @returns A promise that resolves to an array of decisions.
*/
async getDecisionHistory() {
return [...this.decisions];
}
/**
* Clears the decision history in the in-memory store.
*/
async clearDecisionHistory() {
this.decisions = [];
}
};
// src/localStorageDecisionAdapter.ts
var LocalStorageDecisionAdapter = class {
constructor() {
this.STORAGE_KEY = "ivl_decision_history";
}
/**
* Saves a decision to the local storage.
* @param decision - The decision to save.
*/
async saveDecision(decision) {
try {
const history = await this.getDecisionHistory();
history.push(decision);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(history));
} catch (error) {
console.error("Failed to save decision to localStorage:", error);
throw new Error("Decision storage failed");
}
}
/**
* Retrieves the decision history from local storage.
* @returns A promise that resolves to an array of decisions.
*/
async getDecisionHistory() {
try {
const history = localStorage.getItem(this.STORAGE_KEY);
return history ? JSON.parse(history) : [];
} catch (error) {
console.error("Failed to get decision history from localStorage:", error);
throw new Error("Decision retrieval failed");
}
}
/**
* Clears the decision history in local storage.
*/
async clearDecisionHistory() {
try {
localStorage.removeItem(this.STORAGE_KEY);
} catch (error) {
console.error("Failed to clear decision history from localStorage:", error);
throw new Error("Failed to clear decision history");
}
}
};
// src/style.ts
var FALLBACK_CSS = `
.ivl-player-container {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
}
.ivl-player-container h1,
.ivl-player-container h2,
.ivl-player-container h3,
.ivl-player-container h4,
.ivl-player-container h5,
.ivl-player-container h6 {
color: #000;
margin-top: 1em;
margin-bottom: 0.5em;
}
.ivl-player-container {
position: relative;
max-width: 800px;
width: 100%;
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
video {
width: 100%;
display: block;
}
.ivl-interaction-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
z-index: 10;
width: 80%;
max-width: 400px;
}
.ivl-interaction-overlay h3 {
margin-top: 0;
color: #4CAF50;
}
.ivl-interaction-overlay button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
margin-top: 15px;
cursor: pointer;
border-radius: 5px;
}
.ivl-interaction-overlay button:hover {
background-color: #45a049;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
}
button,
select {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
`;
// src/subtitlesManager.ts
var SubtitlesManager = class {
/**
* Creates an instance of SubtitlesManager.
* @param subtitlesElement - The HTML track element for subtitles.
* @param cuePoints - Optional array of cue points to initialize with.
*/
constructor(subtitlesElement, cuePoints = []) {
this.cuePoints = [];
this.subtitlesElement = subtitlesElement;
this.cuePoints = cuePoints;
}
/**
* Registers a callback to be invoked when subtitles are loaded or an error occurs.
* @param callback - A function to be called with the loaded cues or the initial cues if an error occurs.
*/
onLoad(callback) {
this.subtitlesElement.addEventListener("load", () => {
const track = this.subtitlesElement.track;
if (!track) {
console.warn("Subtitle track not available");
callback(this.cuePoints);
return;
}
const trackCues = Array.from(track.cues || []);
const cues = this.cuePoints.map((cueConfig) => ({
...cueConfig,
time: trackCues.find((cue) => cue?.text === cueConfig.subtitleText)?.startTime || 0
}));
callback(cues);
});
this.subtitlesElement.addEventListener("error", (error) => {
console.error("Failed to load subtitles:", error);
callback(this.cuePoints);
});
}
};
// src/player.ts
var IVLabsPlayer = class {
constructor(target, config) {
const targetElement = document.getElementById(target);
if (!targetElement) throw new Error(`Target container with ID "${target}" not found.`);
this.config = config;
this.videoContainer = document.createElement("div");
this.videoContainer.className = "ivl-player-container";
this.videoElement = document.createElement("video");
this.videoElement.controls = true;
this.videoElement.controlsList = "nodownload";
if (config.subtitlesUrl) {
this.subtitlesElement = document.createElement("track");
this.subtitlesElement.src = config.subtitlesUrl;
this.subtitlesElement.kind = "subtitles";
this.subtitlesElement.default = true;
this.subtitlesManager = new SubtitlesManager(this.subtitlesElement, config.cues);
this.videoElement.appendChild(this.subtitlesElement);
this.subtitlesManager.onLoad((cues) => {
this.start(cues);
});
}
this.videoContainer.appendChild(this.videoElement);
targetElement.innerHTML = "";
targetElement.appendChild(this.videoContainer);
this._injectFallbackCss();
this.i18n = new I18n();
if (config.translations) {
for (const [locale, translations] of Object.entries(config.translations)) {
this.i18n.load(locale, translations);
}
}
if (config.locale) {
this.i18n.setLocale(config.locale);
}
if (config.decisionAdapter) {
this.decisionAdapter = config.decisionAdapter;
} else if (config.decisionAdapterType === "localStorage") {
this.decisionAdapter = new LocalStorageDecisionAdapter();
} else {
this.decisionAdapter = new InMemoryDecisionAdapter();
}
this.analytics = new Analytics();
this.stateMachine = new StateMachine(config.initialState || "idle");
this.interactionManager = new InteractionManager(
this.videoContainer,
this.i18n,
this.decisionAdapter
);
this.cueHandler = new CueHandler(this.videoElement);
this.segmentManager = new SegmentManager(this.videoElement);
if (!config.subtitlesUrl) {
this.start(config.cues);
}
}
start(cues) {
this.cueHandler.registerCues(cues || []);
if (!this.config.videoUrl) throw new Error("videoUrl must be provided in the PlayerConfig.");
this.videoElement.src = this.config.videoUrl;
this.bindEvents();
this.cueHandler.start();
}
/**
* Binds event listeners for cue handling, interaction response, and analytics.
*/
bindEvents() {
this.cueHandler.onCue((cue) => {
this.analytics.track("onCueEnter", {
event: "onCueEnter",
cueId: cue.id,
timestamp: Date.now()
});
if (cue.payload?.interaction) {
this.videoElement.pause();
this.stateMachine.transitionTo("waitingForInteraction");
this.interactionManager.handleInteractionCue(cue);
this.analytics.track("onPromptShown", {
event: "onPromptShown",
cueId: cue.id,
timestamp: Date.now()
});
} else {
this.stateMachine.transitionTo("playing");
}
});
this.interactionManager.onResponse((response, cue) => {
this.analytics.track("onInteractionSelected", {
event: "onInteractionSelected",
cueId: cue.id,
data: { response },
timestamp: Date.now()
});
if (response && response.nextSegment) {
this.analytics.track("onBranchJump", {
event: "onBranchJump",
cueId: cue.id,
data: { nextSegment: response.nextSegment },
timestamp: Date.now()
});
this.segmentManager.playSegment(response.nextSegment);
} else {
this.videoElement.play().catch((error) => {
console.error("Video playback failed:", error);
});
}
console.log("Interaction response received:", response);
this.stateMachine.transitionTo("playing");
this.analytics.track("INTERACTION_COMPLETED", {
event: "INTERACTION_COMPLETED",
cueId: cue.id,
data: { response },
timestamp: Date.now()
});
});
this.videoElement.addEventListener("play", () => {
this.analytics.track("VIDEO_STARTED", {
event: "VIDEO_STARTED",
timestamp: Date.now()
});
});
this.videoElement.addEventListener("pause", () => {
this.analytics.track("VIDEO_PAUSED", {
event: "VIDEO_PAUSED",
timestamp: Date.now()
});
});
this.videoElement.addEventListener("ended", () => {
this.analytics.track("VIDEO_ENDED", {
event: "VIDEO_ENDED",
timestamp: Date.now()
});
});
}
/** Loads cue points into the player. */
loadCues(cues) {
this.cueHandler.loadCues(cues);
}
/** Loads interaction segments into the player. */
loadInteractions(interactions) {
this.interactionManager.loadInteractions(interactions);
}
/** Returns the current player state. */
getState() {
return this.stateMachine.getState();
}
/** Plays the video. */
play() {
this.videoElement.play();
}
/** Pauses the video. */
pause() {
this.videoElement.pause();
}
/**
* Registers a custom analytics hook for a specific event.
* @param event - The event to listen for.
* @param callback - The function to be called when the event is tracked.
*/
on(event, callback) {
this.analytics.on(event, (event2, payload) => callback(payload));
}
/**
* Sets the current language for localization.
* @param locale - The new locale.
*/
setLocale(locale) {
this.i18n.setLocale(locale);
}
/**
* Loads a set of translations for a specific locale.
* @param locale - The locale for the translations.
* @param translations - The translation data.
*/
loadTranslations(locale, translations) {
this.i18n.load(locale, translations);
}
/**
* Retrieves the user's decision history.
* @returns A promise that resolves to an array of Decision objects.
*/
getDecisionHistory() {
return this.decisionAdapter.getDecisionHistory();
}
/**
* Clears the user's decision history.
* @returns A promise that resolves when the history is cleared.
*/
clearDecisionHistory() {
return this.decisionAdapter.clearDecisionHistory();
}
/**
* Injects fallback CSS into the document head.
* This ensures basic styling is present even if external stylesheets are not loaded.
*/
_injectFallbackCss() {
const style = document.createElement("style");
style.textContent = FALLBACK_CSS;
document.head.appendChild(style);
}
/** Cleans up the player, removes listeners and resets state. */
destroy() {
console.log("IVLabsPlayer destroy() called.");
this.cueHandler.destroy();
this.interactionManager.destroy();
this.analytics.track("onSessionEnd", {
event: "onSessionEnd",
timestamp: Date.now()
});
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Analytics,
CueHandler,
IVLabsPlayer,
InteractionManager,
StateMachine
});