+ constructor(document, texture) {
+ this.document = document;
+ this.uuid = this.document.uuid;
+ this.flags = new Flags(this.document);
+ this.offset = this.flags.offset;
+ this.texture = texture;
+ this.video = this.texture.baseTexture.resource.source;
+ this.timeout = null;
+ this.still = false;
+ this.playing = false;
+ this.ignoreDate = false;
+ this.nextButton = false;
+ this.prevButton = false;
+ this.select = false;
+ this.newCurrentTime = null;
+ this.randomTimers = {};
+ this.ready = !!currentDelegator;
+ }
+
+ static setAllReady() {
+ this.getAll().forEach(statefulVideo => {
+ if (!statefulVideo.ready) {
+ statefulVideo.ready = true;
+ statefulVideo.flags.updateData();
+ statefulVideo.setupRandomTimers();
+ statefulVideo.play();
+ }
+ });
+ }
+
+ static determineCurrentDelegator() {
+
+ if (delegateDebounce) delegateDebounce();
+
+ delegateDebounce = foundry.utils.debounce(async () => {
+
+ // When you first render a scene, determine which user should be the delegator
+ const newDelegator = lib.getSceneDelegator();
+
+ // If the user isn't the delegator, they should clear their own info to avoid confusion
+ if (!game.user.isGM && newDelegator !== game.user && lib.isGMConnected()) {
+ await game.user.unsetFlag(CONSTANTS.MODULE_NAME, CONSTANTS.FLAG_KEYS.DELEGATED_STATEFUL_VIDEOS);
+ }
+
+ // If the delegator has changed to a non-GM, and the new delegator is you, whilst there are no GMs connected
+ if (newDelegator !== currentDelegator && !newDelegator.isGM && newDelegator === game.user && !lib.isGMConnected()) {
+
+ // Grab all stateful video's current state
+ let updates = {};
+ StatefulVideo.getAll().forEach(statefulVideo => {
+ updates[CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG + "." + statefulVideo.delegationUuid] = statefulVideo.flags.getData();
+ });
+
+ currentDelegator = newDelegator;
+
+ // Store the stateful video's current state on yourself
+ await game.user.update(updates);
+
+ }
+
+ currentDelegator = newDelegator;
+
+ StatefulVideo.setAllReady();
+
+ }, 100);
+
+ }
+
+ static registerHooks() {
+
+ Hooks.on('userConnected', () => {
+ this.determineCurrentDelegator();
+ });
+
+ Hooks.on('getSceneNavigationContext', () => {
+ this.determineCurrentDelegator();
+ });
+
+ let firstUpdate = true;
+ Hooks.on('updateUser', (user, data) => {
+
+ // If the user wasn't updated with delegated stateful videos, exit
+ if (!foundry.utils.hasProperty(data, CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG)) return;
+
+ // If they were, but it was removed, exit
+ const statefulVideos = foundry.utils.getProperty(data, CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG);
+ if (!statefulVideos) return;
+
+ // If the current delegator is a GM, don't do anything, they will handle updates
+ if (currentDelegator.isGM) return;
+
+ // Otherwise, loop through each of the updated stateful videos
+ Object.keys(statefulVideos).forEach(key => {
+ // Get the stateful video based on the UUID that was updated on the user
+ const statefulVideo = StatefulVideo.get(`Scene.${key.split("_").join(".")}`);
+ if (!statefulVideo) return;
+ // Call the update method, and pass the user that is the current delegator
+ StatefulVideo.onUpdate(statefulVideo.document, // Construct a similar diff as normal video updates would create
+ foundry.utils.mergeObject({
+ [CONSTANTS.FLAGS]: statefulVideos[key]
+ }, {}), firstUpdate);
+ });
+ firstUpdate = false;
+ });
+
+ Hooks.on("renderBasePlaceableHUD", (app, html) => {
+ statefulVideoHudMap.set(app.object.document.uuid, app);
+ StatefulVideo.renderStatefulVideoHud(app, html);
+ });
+
+ Hooks.on("preUpdateTile", (placeableDoc, data) => {
+ StatefulVideo.onPreUpdate(placeableDoc, data);
+ });
+
+ Hooks.on("updateTile", (placeableDoc, data) => {
+ StatefulVideo.onUpdate(placeableDoc, data);
+ });
+
+ Hooks.on("preUpdateToken", (placeableDoc, data) => {
+ StatefulVideo.onPreUpdate(placeableDoc, data);
+ });
+
+ Hooks.on("updateToken", (placeableDoc, data) => {
+ StatefulVideo.onUpdate(placeableDoc, data);
+ });
+
+ Hooks.on("createTile", (placeableDoc) => {
+ StatefulVideo.createPlaceable(placeableDoc);
+ });
+
+ Hooks.on("createToken", (placeableDoc) => {
+ StatefulVideo.createPlaceable(placeableDoc);
+ });
+
+ let firstUserGesture = false;
+ let canvasReady = false
+ Hooks.once("canvasFirstUserGesture", () => {
+ firstUserGesture = true;
+ if (canvasReady) {
+ StatefulVideo.canvasReady();
+ }
+ });
+
+ Hooks.on("canvasReady", () => {
+ canvasReady = true;
+ if (firstUserGesture) {
+ StatefulVideo.canvasReady();
+ } else {
+ StatefulVideo.canvasNotReady();
+ }
+ })
+
+ Hooks.on("canvasPan", () => {
+ hudScale.set(canvas.stage.scale.x);
+ });
+
+ hudScale.subscribe(() => {
+ StatefulVideo.getAll().forEach(statefulVideo => statefulVideo.updateHudScale());
+ });
+
+ const refreshDebounce = foundry.utils.debounce((statefulVideo) => {
+ if (game?.video && statefulVideo.video) {
+ statefulVideo.updateVideo();
+ statefulVideo.play();
+ }
+ }, 200);
+
+ Hooks.on("refreshTile", (placeableObject) => {
+ if (!placeableObject.isVideo || !foundry.utils.getProperty(placeableObject.document, CONSTANTS.STATES_FLAG)?.length) return;
+ const statefulVideo = StatefulVideo.make(placeableObject.document, placeableObject.texture);
+ if (!statefulVideo) return;
+ statefulVideo.evaluateVisibility();
+ refreshDebounce(statefulVideo);
+ });
+
+ Hooks.on("refreshToken", (placeableObject) => {
+ if (!placeableObject.isVideo || !foundry.utils.getProperty(placeableObject.document, CONSTANTS.STATES_FLAG)?.length) return;
+ const statefulVideo = StatefulVideo.make(placeableObject.document, placeableObject.texture);
+ if (!statefulVideo) return;
+ statefulVideo.evaluateVisibility();
+ refreshDebounce(statefulVideo);
+ });
+
+ }
+
+ static createPlaceable(placeableDoc) {
+ if (!lib.isResponsibleGM()) return;
+ const path = lib.getVideoJsonPath(placeableDoc);
+ fetch(path)
+ .then(response => response.json())
+ .then((result) => {
+ const states = foundry.utils.getProperty(result, CONSTANTS.STATES_FLAG);
+ result[CONSTANTS.CURRENT_STATE_FLAG] = states.findIndex(s => s.default);
+ const currentState = states[result[CONSTANTS.CURRENT_STATE_FLAG]];
+ if (result[CONSTANTS.FOLDER_PATH_FLAG] && currentState.file) {
+ result["texture.src"] = result[CONSTANTS.FOLDER_PATH_FLAG] + "/" + currentState.file;
+ }
+ placeableDoc.update(result);
+ })
+ .catch(err => {
+ console.error(err)
+ });
+ }
+
+ static getValidPlaceables() {
+ return [...canvas.tiles.placeables, canvas.tokens.placeables].filter(placeable => {
+ return placeable.isVideo && foundry.utils.getProperty(placeable.document, CONSTANTS.STATES_FLAG)?.length;
+ });
+ }
+
+ static canvasReady() {
+ hudScale.set(canvas.stage.scale.x);
+ for (const placeable of this.getValidPlaceables()) {
+ const statefulVideo = this.make(placeable.document, placeable.texture);
+ if (!statefulVideo) return;
+ if (game?.video && statefulVideo.video) {
+ statefulVideo.play();
+ }
+ }
+ }
+
+ static canvasNotReady() {
+ for (const placeable of this.getValidPlaceables()) {
+ placeable.renderable = false;
+ placeable.mesh.renderable = false;
+ }
+ }
+
+ static getAll() {
+ return managedStatefulVideos;
+ }
+
+ static get(uuid) {
+ return managedStatefulVideos.get(uuid) || false;
+ }
+
+ static make(document, texture) {
+ const existingStatefulVideo = this.get(document.uuid);
+ if (existingStatefulVideo) return existingStatefulVideo;
+ const newStatefulVideo = new this(document, texture);
+ managedStatefulVideos.set(newStatefulVideo.uuid, newStatefulVideo);
+ if (currentDelegator) {
+ newStatefulVideo.flags.updateData();
+ }
+ return newStatefulVideo;
+ }
+
+ get duration() {
+ return Math.max(0, (this.video.duration * 1000) - this.flags.singleFrameDuration);
+ }
+
+ static tearDown(uuid) {
+ const statefulVideo = StatefulVideo.get(uuid);
+ if (!statefulVideo) return;
+ if (statefulVideo.timeout) clearTimeout(statefulVideo.timeout);
+ statefulVideo.clearRandomTimers();
+ managedStatefulVideos.delete(uuid);
+ }
+
+ static makeHudButton(tooltip, icon, style = "") {
+ return $(`
`);
- }
+ }
- static makeStateSelect(statefulVideo) {
- if (!statefulVideo.flags.states.length) return false;
- const select = $("
");
- select.on('change', function () {
- statefulVideo.changeState({ state: Number($(this).val()), fast: true });
- });
+ static makeStateSelect(statefulVideo) {
+ if (!statefulVideo.flags.states.length) return false;
+ const select = $("
");
+ select.on('change', function () {
+ statefulVideo.changeState({ state: Number($(this).val()), fast: true });
+ });
- for (const [index, state] of statefulVideo.flags.states.entries()) {
- select.append(`
`);
- }
- return select;
- }
+ for (const [index, state] of statefulVideo.flags.states.entries()) {
+ select.append(`
`);
+ }
+ return select;
+ }
- /**
- * Adds additional control elements to the tile HUD relating to Animated Tile States
- *
- * @param app
- * @param html
- */
- static async renderStatefulVideoHud(app, html) {
+ /**
+ * Adds additional control elements to the tile HUD relating to Animated Tile States
+ *
+ * @param app
+ * @param html
+ */
+ static async renderStatefulVideoHud(app, html) {
- const placeable = app.object;
- const placeableDocument = placeable.document;
- const statefulVideo = StatefulVideo.get(placeableDocument.uuid);
+ const placeable = app.object;
+ const placeableDocument = placeable.document;
+ const statefulVideo = StatefulVideo.get(placeableDocument.uuid);
- const root = $("
");
- if (statefulVideo) {
+ if (statefulVideo) {
- const iconContainer = $("
");
- selectContainer.append(iconContainer);
+ const iconContainer = $("
");
+ selectContainer.append(iconContainer);
- for (const [index, state] of statefulVideo.flags.states.entries()) {
- if (!state.icon) continue;
- const stateBtn = StatefulVideo.makeHudButton(state.name, state.icon);
- stateBtn.on("pointerdown", () => {
- statefulVideo.changeState({ state: index, fast: true });
- });
- iconContainer.append(stateBtn);
- }
+ for (const [index, state] of statefulVideo.flags.states.entries()) {
+ if (!state.icon) continue;
+ const stateBtn = StatefulVideo.makeHudButton(state.name, state.icon);
+ stateBtn.on("pointerdown", () => {
+ statefulVideo.changeState({ state: index, fast: true });
+ });
+ iconContainer.append(stateBtn);
+ }
- if (statefulVideo.flags.clientDropdown && statefulVideo.flags.states.length) {
- const select = StatefulVideo.makeStateSelect(statefulVideo);
- selectContainer.append(select);
- statefulVideo.select = select;
- }
- }
+ if (statefulVideo.flags.clientDropdown && statefulVideo.flags.states.length) {
+ const select = StatefulVideo.makeStateSelect(statefulVideo);
+ selectContainer.append(select);
+ statefulVideo.select = select;
+ }
+ }
- const statefulVideoColor = lib.determineFileColor(placeableDocument.texture?.src || "");
+ const statefulVideoColor = lib.determineFileColor(placeableDocument.texture?.src || "");
- const selectButtonContainer = $("
+ const selectColorButton = $(`
`);
- const fileSearchQuery = lib.getCleanWebmPath(placeableDocument)
- .replace(".webm", "*.webm")
-
- const baseVariation = placeableDocument.texture.src.includes("_(")
- ? placeableDocument.texture.src.split("_(")[1].split(")")[0]
- : false;
-
- const internalVariation = placeableDocument.texture.src.includes("_%5B")
- ? placeableDocument.texture.src.split("_%5B")[1].split("%5D")[0]
- : false;
-
- const colorVariation = placeableDocument.texture.src.includes("__")
- ? placeableDocument.texture.src.split("__")[1].split(".")[0]
- : false;
-
- await lib.getWildCardFiles(fileSearchQuery).then((results) => {
-
- const nonThumbnails = results.filter(filePath => !filePath.includes("_thumb")).sort((a, b) => {
- const a_value = (a.includes("_%5B")) + (a.includes("_(") * 10) + (a.includes("__") * 100);
- const b_value = (b.includes("_%5B")) + (b.includes("_(") * 10) + (b.includes("__") * 100);
- return a_value - b_value;
- });
-
- const internalVariations = nonThumbnails.filter(filePath => {
- return (!colorVariation && !filePath.includes("__") || (colorVariation && filePath.includes(`__${colorVariation}`))) && (
- (!baseVariation && !filePath.includes("_("))
- ||
- (baseVariation && filePath.includes(`_(${baseVariation})`))
- );
- })
-
- const colorVariations = Object.values(nonThumbnails.reduce((acc, filePath) => {
- if (internalVariation && !filePath.includes(`_%5B${internalVariation}%5D`)) return acc;
- const colorConfig = lib.determineFileColor(filePath);
- if (!acc[colorConfig.colorName]) {
- acc[colorConfig.colorName] = {
- ...colorConfig,
- filePath
- };
- }
- return acc;
- }, {}));
-
- if (internalVariations.length <= 1 && colorVariations.length <= 1) return;
-
- const parentContainer = $(`
`);
-
- const width = internalVariations.length > 1 ? 300 : Math.min(204, colorVariations.length * 34);
- parentContainer.css({ left: width * -0.4, width });
-
- selectColorButton.on('pointerdown', () => {
- const newState = parentContainer.css('visibility') === "hidden" ? "visible" : "hidden";
- parentContainer.css("visibility", newState);
- });
-
- const colorContainer = $(`
`);
- const variationContainer = $(`
`);
-
- selectContainer.append(selectButtonContainer);
- selectButtonContainer.append(selectColorButton);
- selectButtonContainer.append(parentContainer);
- parentContainer.append(variationContainer);
- parentContainer.append(colorContainer);
-
- for (const variation of internalVariations) {
- const name = variation.includes("_%5B") ? variation.split("%5B")[1].split("%5D")[0] : "original";
- const button = $(`
${name}`)
- variationContainer.append(button);
- button.on("pointerdown", async () => {
- await placeableDocument.update({
- "texture.src": variation
- });
- const hud = placeable instanceof Token
- ? canvas.tokens.hud
- : canvas.tiles.hud;
- placeable.control();
- hud.bind(placeable);
- });
- }
-
- for (const colorConfig of colorVariations) {
- const { colorName, color, tooltip, filePath } = colorConfig;
- const button = $(`
`)
- if (!colorName) {
- colorContainer.prepend(button);
- } else {
- colorContainer.append(button);
- }
- button.on("pointerdown", async () => {
- selectColorButton.html(`
`);
- selectColorButton.trigger("pointerdown");
- await placeableDocument.update({
- "texture.src": filePath
- });
- const hud = placeable instanceof Token
- ? canvas.tokens.hud
- : canvas.tiles.hud;
- placeable.control();
- hud.bind(placeable);
- });
- }
- });
-
- if (statefulVideo || selectButtonContainer.children().length) {
- root.append(selectContainer);
- }
-
- if (statefulVideo) {
- statefulVideo.updateHudScale();
- }
-
- Hooks.call(CONSTANTS.HOOKS.RENDER_UI, app, root, placeableDocument, statefulVideo);
-
- if (root.children().length) {
- html.find(".col.middle").append(root);
- }
-
- }
-
- play() {
- if (!this.document.autoplay) return;
- return game.video.play(this.video);
- }
-
- updateVideo() {
- if (!this.document.object) return;
- this.texture = this.document.object.texture;
- this.video = this.document.object.texture.baseTexture.resource.source;
- }
-
- updateHudScale() {
- if (!this.select) return;
- const scale = get(hudScale) + 0.25;
- const fontSize = scale >= 1.0 ? 1.0 : Math.min(1.0, Math.max(0.25, lib.transformNumber(scale)))
- this.select.children().css("font-size", `${fontSize}rem`)
- }
-
- updateSelect() {
- if (!this.select?.length) return;
- this.select.empty();
- for (const [index, state] of this.flags.states.entries()) {
- this.select.append(`
`)
- }
- this.updateHudScale();
- }
-
- static onPreUpdate(placeableDoc, changes) {
- let statefulVideo = StatefulVideo.get(placeableDoc.uuid);
- const diff = foundry.utils.diffObject(placeableDoc, changes);
- if (foundry.utils.hasProperty(diff, "texture.src") && statefulVideo) {
- statefulVideo.newCurrentTime = statefulVideo.video.currentTime * 1000;
- }
- }
-
- static onUpdate(placeableDoc, changes, firstUpdate = false) {
- let statefulVideo = StatefulVideo.get(placeableDoc.uuid);
- if (foundry.utils.hasProperty(changes, "texture.src") && statefulVideo) {
- setTimeout(() => {
- statefulVideo.texture = placeableDoc.object.texture;
- statefulVideo.video = placeableDoc.object.texture.baseTexture.resource.source;
- statefulVideo.still = false;
- statefulVideo.playing = false;
- clearTimeout(statefulVideo.timeout);
- statefulVideo.play();
- }, 100);
- }
- if (!foundry.utils.hasProperty(changes, CONSTANTS.FLAGS)) return;
- if (!statefulVideo) {
- if (!placeableDoc.object.isVideo || !foundry.utils.getProperty(placeableDoc, CONSTANTS.STATES_FLAG)?.length) return;
- statefulVideo = StatefulVideo.make(placeableDoc, placeableDoc.object.texture);
- }
- statefulVideo.flags.updateData();
- Hooks.call("ats.updateState", placeableDoc, statefulVideo.flags.data, changes);
- if (!statefulVideo.flags.states.length) {
- this.tearDown(placeableDoc.uuid);
- statefulVideoHudMap.get(placeableDoc.uuid)?.render(true);
- return;
- }
- statefulVideo.offset = Number(Date.now()) - statefulVideo.flags.updated;
- if (foundry.utils.hasProperty(changes, CONSTANTS.STATES_FLAG)) {
- statefulVideoHudMap.get(placeableDoc.uuid)?.render(true);
- statefulVideo.still = false;
- statefulVideo.playing = false;
- statefulVideo.clearRandomTimers();
- statefulVideo.setupRandomTimers();
- clearTimeout(statefulVideo.timeout);
- statefulVideo.play();
- statefulVideo.flags.data.queuedState = statefulVideo.flags.determineNextStateIndex();
- return placeableDoc.update({
- [CONSTANTS.CURRENT_STATE_FLAG]: statefulVideo.flags.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE
- ? statefulVideo.flags.data.queuedState
- : statefulVideo.flags.data.currentStateIndex,
- [CONSTANTS.QUEUED_STATE_FLAG]: statefulVideo.flags.data.queuedState
- });
- }
- statefulVideo.updateSelect();
- if (foundry.utils.hasProperty(changes, CONSTANTS.CURRENT_STATE_FLAG) || firstUpdate || statefulVideo.flags.previousState.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE) {
- statefulVideo.setupRandomTimers();
- if (statefulVideo.nextButton) {
- statefulVideo.nextButton.removeClass("active");
- }
- if (statefulVideo.prevButton) {
- statefulVideo.prevButton.removeClass("active");
- }
- statefulVideo.still = false;
- statefulVideo.playing = false;
- statefulVideo.play();
- }
- }
-
- static isDataValid(flags, data) {
- const previousStateFlagDifferent = (data?.[CONSTANTS.PREVIOUS_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.PREVIOUS_STATE] !== data?.[CONSTANTS.PREVIOUS_STATE_FLAG]));
- const currentStateFlagDifferent = (data?.[CONSTANTS.CURRENT_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.CURRENT_STATE] !== data?.[CONSTANTS.CURRENT_STATE_FLAG]));
- const queuedStateFlagDifferent = (data?.[CONSTANTS.QUEUED_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.QUEUED_STATE] !== data?.[CONSTANTS.QUEUED_STATE_FLAG]));
- const previousFlagIsRandomState = flags.data.states[(data?.[CONSTANTS.PREVIOUS_STATE_FLAG] ?? flags.data[CONSTANTS.FLAG_KEYS.CURRENT_STATE])]?.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE;
- return previousStateFlagDifferent || currentStateFlagDifferent || queuedStateFlagDifferent || previousFlagIsRandomState;
- }
-
- async update(data) {
- if (game.user !== currentDelegator) return;
-
- if (!StatefulVideo.isDataValid(this.flags, data)) return;
-
- data[CONSTANTS.UPDATED_FLAG] = data[CONSTANTS.UPDATED_FLAG] ?? Number(Date.now());
-
- if (game.user.isGM) {
- return this.document.update(data);
- } else if (lib.getResponsibleGM()) {
- return SocketHandler.emit(SocketHandler.UPDATE_PLACEABLE_DOCUMENT, {
- uuid: this.uuid, update: data, userId: lib.getResponsibleGM().id
- });
- }
-
- const deconstructedData = Object.fromEntries(Object.entries(data)
- .map(([key, value]) => {
- const newKey = key.split(".");
- return [newKey[newKey.length - 1], value];
- }));
-
- const key = `${this.document.parent.id}_${this.document.documentName}_${this.document.id}`;
- return game.user.update({
- [`${CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG}.${key}`]: deconstructedData
- });
- }
-
- async queueState(newState) {
- const updates = {
- [CONSTANTS.QUEUED_STATE_FLAG]: newState
- };
- if (Hooks.call("ats.preUpdateQueuedState", this.document, this.flags.data, updates) === false) {
- return;
- }
- return this.update(updates);
- }
-
- async updateState(stateIndex) {
- let previousStateIndex = this.flags.currentStateIndex;
- if (this.flags.states[stateIndex].behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE) {
- previousStateIndex = stateIndex;
- stateIndex = this.flags.determineNextStateIndex(stateIndex);
- }
- const updates = {
- [CONSTANTS.UPDATED_FLAG]: Number(Date.now()),
- [CONSTANTS.PREVIOUS_STATE_FLAG]: previousStateIndex,
- [CONSTANTS.CURRENT_STATE_FLAG]: stateIndex,
- [CONSTANTS.QUEUED_STATE_FLAG]: this.flags.determineNextStateIndex(stateIndex),
- "texture.src": this.flags.determineFile(stateIndex)
- };
- if (Hooks.call("ats.preUpdateCurrentState", this.document, this.flags.data, updates) === false) {
- return;
- }
- return this.update(updates);
- }
-
- async changeState({ state = null, step = 1, fast = false } = {}) {
-
- if (this.nextButton) {
- this.nextButton.removeClass("active");
- }
- if (this.prevButton) {
- this.prevButton.removeClass("active");
- }
-
- this.clearRandomTimers();
-
- if (!fast && this.flags.currentState.behavior !== CONSTANTS.BEHAVIORS.STILL) {
- if (this.nextButton && this.prevButton && state === null) {
- this[step > 0 ? "nextButton" : "prevButton"].addClass("active");
- }
- return this.queueState(state ?? this.flags.currentStateIndex + step);
- }
-
- clearTimeout(this.timeout);
- this.timeout = null;
-
- const currentStateIndex = this.flags.currentStateIndex;
-
- return this.updateState(state ?? currentStateIndex + step).then(() => {
- if (currentStateIndex !== state) return;
- SocketHandler.emit(SocketHandler.REPLAY_CURRENT_STATE, { uuid: this.uuid, userId: game.userId });
- this.replayCurrentState();
- });
-
- }
-
- setupRandomTimers() {
-
- if (game.user !== currentDelegator) return;
-
- if (!(
- this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL
- || this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN
- || this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.LOOP
- )) {
- return false;
- }
-
- for (const stateIndex of this.flags.determineNextRandomStates()) {
- if (this.randomTimers[stateIndex]) continue;
- const state = this.flags.states[stateIndex];
- const delayStart = Number(state.randomStart) * 1000;
- const delayEnd = Number(state.randomEnd) * 1000;
- const delay = lib.randomIntegerBetween(delayStart, delayEnd);
- if (CONFIG.debug.kinemancer) ui.notifications.notify(`Next random state in ${delay / 1000}s`)
- let timerId = null;
- timerId = setTimeout(() => {
- delete this.randomTimers[stateIndex];
- if (this.flags.currentStateIsRandom) return;
- if (this.flags.currentStateIsStill) {
- this.updateState(stateIndex);
- } else if (this.flags.currentStateIsLoop) {
- this.queueState(stateIndex);
- }
- }, delay)
- this.randomTimers[stateIndex] = timerId;
- }
-
- }
-
- clearRandomTimers() {
- Object.values(this.randomTimers).forEach(timerId => clearTimeout(timerId));
- this.randomTimers = {};
- }
-
- determineStartTime(stateIndex) {
-
- const currState = this.flags.states?.[stateIndex];
- const currStart = lib.isRealNumber(currState?.start)
- ? Number(currState?.start) * this.flags.durationMultiplier
- : (currState?.start ?? 0);
-
- switch (currStart) {
-
- case CONSTANTS.START.START:
- return 0;
-
- case CONSTANTS.START.END:
- return this.duration;
-
- case CONSTANTS.START.MID:
- return Math.floor(this.duration / 2);
+ const fileSearchQuery = lib.getCleanWebmPath(placeableDocument)
+ .replace(".webm", "*.webm")
+
+ const baseVariation = placeableDocument.texture.src.includes("_(")
+ ? placeableDocument.texture.src.split("_(")[1].split(")")[0]
+ : false;
+
+ const internalVariation = placeableDocument.texture.src.includes("_%5B")
+ ? placeableDocument.texture.src.split("_%5B")[1].split("%5D")[0]
+ : false;
+
+ const colorVariation = placeableDocument.texture.src.includes("__")
+ ? placeableDocument.texture.src.split("__")[1].split(".")[0]
+ : false;
+
+ await lib.getWildCardFiles(fileSearchQuery).then((results) => {
+
+ const nonThumbnails = results.filter(filePath => !filePath.includes("_thumb")).sort((a, b) => {
+ const a_value = (a.includes("_%5B")) + (a.includes("_(") * 10) + (a.includes("__") * 100);
+ const b_value = (b.includes("_%5B")) + (b.includes("_(") * 10) + (b.includes("__") * 100);
+ return a_value - b_value;
+ });
+
+ const internalVariations = nonThumbnails.filter(filePath => {
+ return (!colorVariation && !filePath.includes("__") || (colorVariation && filePath.includes(`__${colorVariation}`))) && (
+ (!baseVariation && !filePath.includes("_("))
+ ||
+ (baseVariation && filePath.includes(`_(${baseVariation})`))
+ );
+ })
+
+ const colorVariations = Object.values(nonThumbnails.reduce((acc, filePath) => {
+ if (internalVariation && !filePath.includes(`_%5B${internalVariation}%5D`)) return acc;
+ const colorConfig = lib.determineFileColor(filePath);
+ if (!acc[colorConfig.colorName]) {
+ acc[colorConfig.colorName] = {
+ ...colorConfig,
+ filePath
+ };
+ }
+ return acc;
+ }, {}));
+
+ if (internalVariations.length <= 1 && colorVariations.length <= 1) return;
+
+ const parentContainer = $(`
`);
+
+ const width = internalVariations.length > 1 ? 300 : Math.min(204, colorVariations.length * 34);
+ parentContainer.css({ left: width * -0.4, width });
+
+ selectColorButton.on('pointerdown', () => {
+ const newState = parentContainer.css('visibility') === "hidden" ? "visible" : "hidden";
+ parentContainer.css("visibility", newState);
+ });
+
+ const colorContainer = $(`
`);
+ const variationContainer = $(`
`);
+
+ selectContainer.append(selectButtonContainer);
+ selectButtonContainer.append(selectColorButton);
+ selectButtonContainer.append(parentContainer);
+ parentContainer.append(variationContainer);
+ parentContainer.append(colorContainer);
+
+ for (const variation of internalVariations) {
+ const name = variation.includes("_%5B") ? variation.split("%5B")[1].split("%5D")[0] : "original";
+ const button = $(`
${name}`)
+ variationContainer.append(button);
+ button.on("pointerdown", async () => {
+ await placeableDocument.update({
+ "texture.src": variation
+ });
+ const hud = placeable instanceof Token
+ ? canvas.tokens.hud
+ : canvas.tiles.hud;
+ placeable.control();
+ hud.bind(placeable);
+ });
+ }
+
+ for (const colorConfig of colorVariations) {
+ const { colorName, color, tooltip, filePath } = colorConfig;
+ const button = $(`
`)
+ if (!colorName) {
+ colorContainer.prepend(button);
+ } else {
+ colorContainer.append(button);
+ }
+ button.on("pointerdown", async () => {
+ selectColorButton.html(`
`);
+ selectColorButton.trigger("pointerdown");
+ await placeableDocument.update({
+ "texture.src": filePath
+ });
+ const hud = placeable instanceof Token
+ ? canvas.tokens.hud
+ : canvas.tiles.hud;
+ placeable.control();
+ hud.bind(placeable);
+ });
+ }
+ });
+
+ if (statefulVideo || selectButtonContainer.children().length) {
+ root.append(selectContainer);
+ }
+
+ if (statefulVideo) {
+ statefulVideo.updateHudScale();
+ }
+
+ Hooks.call(CONSTANTS.HOOKS.RENDER_UI, app, root, placeableDocument, statefulVideo);
+
+ if (root.children().length) {
+ html.find(".col.middle").append(root);
+ }
+
+ }
+
+ play() {
+ if (!this.document.video.autoplay) return;
+ return game.video.play(this.video);
+ }
+
+ updateVideo() {
+ if (!this.document.object) return;
+ this.texture = this.document.object.texture;
+ this.video = this.document.object.texture.baseTexture.resource.source;
+ }
+
+ updateHudScale() {
+ if (!this.select) return;
+ const scale = get(hudScale) + 0.25;
+ const fontSize = scale >= 1.0 ? 1.0 : Math.min(1.0, Math.max(0.25, lib.transformNumber(scale)))
+ this.select.children().css("font-size", `${fontSize}rem`)
+ }
+
+ updateSelect() {
+ if (!this.select?.length) return;
+ this.select.empty();
+ for (const [index, state] of this.flags.states.entries()) {
+ this.select.append(`
`)
+ }
+ this.updateHudScale();
+ }
+
+ static onPreUpdate(placeableDoc, changes) {
+ let statefulVideo = StatefulVideo.get(placeableDoc.uuid);
+ const diff = foundry.utils.diffObject(placeableDoc, changes);
+ if (foundry.utils.hasProperty(diff, "texture.src") && statefulVideo) {
+ statefulVideo.newCurrentTime = statefulVideo.video.currentTime * 1000;
+ }
+ }
+
+ static onUpdate(placeableDoc, changes, firstUpdate = false) {
+ let statefulVideo = StatefulVideo.get(placeableDoc.uuid);
+ if (foundry.utils.hasProperty(changes, "texture.src") && statefulVideo) {
+ setTimeout(() => {
+ statefulVideo.texture = placeableDoc.object.texture;
+ statefulVideo.video = placeableDoc.object.texture.baseTexture.resource.source;
+ statefulVideo.still = false;
+ statefulVideo.playing = false;
+ clearTimeout(statefulVideo.timeout);
+ statefulVideo.play();
+ }, 100);
+ }
+ if (!foundry.utils.hasProperty(changes, CONSTANTS.FLAGS)) return;
+ if (!statefulVideo) {
+ if (!placeableDoc.object.isVideo || !foundry.utils.getProperty(placeableDoc, CONSTANTS.STATES_FLAG)?.length) return;
+ statefulVideo = StatefulVideo.make(placeableDoc, placeableDoc.object.texture);
+ }
+ statefulVideo.flags.updateData();
+ Hooks.call("ats.updateState", placeableDoc, statefulVideo.flags.data, changes);
+ if (!statefulVideo.flags.states.length) {
+ this.tearDown(placeableDoc.uuid);
+ statefulVideoHudMap.get(placeableDoc.uuid)?.render(true);
+ return;
+ }
+ statefulVideo.offset = Number(Date.now()) - statefulVideo.flags.updated;
+ if (foundry.utils.hasProperty(changes, CONSTANTS.STATES_FLAG)) {
+ statefulVideoHudMap.get(placeableDoc.uuid)?.render(true);
+ statefulVideo.still = false;
+ statefulVideo.playing = false;
+ statefulVideo.clearRandomTimers();
+ statefulVideo.setupRandomTimers();
+ clearTimeout(statefulVideo.timeout);
+ statefulVideo.play();
+ statefulVideo.flags.data.queuedState = statefulVideo.flags.determineNextStateIndex();
+ return placeableDoc.update({
+ [CONSTANTS.CURRENT_STATE_FLAG]: statefulVideo.flags.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE
+ ? statefulVideo.flags.data.queuedState
+ : statefulVideo.flags.data.currentStateIndex,
+ [CONSTANTS.QUEUED_STATE_FLAG]: statefulVideo.flags.data.queuedState
+ });
+ }
+ statefulVideo.updateSelect();
+ if (foundry.utils.hasProperty(changes, CONSTANTS.CURRENT_STATE_FLAG) || firstUpdate || statefulVideo.flags.previousState.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE) {
+ statefulVideo.setupRandomTimers();
+ if (statefulVideo.nextButton) {
+ statefulVideo.nextButton.removeClass("active");
+ }
+ if (statefulVideo.prevButton) {
+ statefulVideo.prevButton.removeClass("active");
+ }
+ statefulVideo.still = false;
+ statefulVideo.playing = false;
+ statefulVideo.play();
+ }
+ }
+
+ static isDataValid(flags, data) {
+ const previousStateFlagDifferent = (data?.[CONSTANTS.PREVIOUS_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.PREVIOUS_STATE] !== data?.[CONSTANTS.PREVIOUS_STATE_FLAG]));
+ const currentStateFlagDifferent = (data?.[CONSTANTS.CURRENT_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.CURRENT_STATE] !== data?.[CONSTANTS.CURRENT_STATE_FLAG]));
+ const queuedStateFlagDifferent = (data?.[CONSTANTS.QUEUED_STATE_FLAG] !== undefined && (flags.data[CONSTANTS.FLAG_KEYS.QUEUED_STATE] !== data?.[CONSTANTS.QUEUED_STATE_FLAG]));
+ const previousFlagIsRandomState = flags.data.states[(data?.[CONSTANTS.PREVIOUS_STATE_FLAG] ?? flags.data[CONSTANTS.FLAG_KEYS.CURRENT_STATE])]?.behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE;
+ return previousStateFlagDifferent || currentStateFlagDifferent || queuedStateFlagDifferent || previousFlagIsRandomState;
+ }
+
+ async update(data) {
+ if (game.user !== currentDelegator) return;
+
+ if (!StatefulVideo.isDataValid(this.flags, data)) return;
+
+ data[CONSTANTS.UPDATED_FLAG] = data[CONSTANTS.UPDATED_FLAG] ?? Number(Date.now());
+
+ if (game.user.isGM) {
+ return this.document.update(data);
+ } else if (lib.getResponsibleGM()) {
+ return SocketHandler.emit(SocketHandler.UPDATE_PLACEABLE_DOCUMENT, {
+ uuid: this.uuid, update: data, userId: lib.getResponsibleGM().id
+ });
+ }
+
+ const deconstructedData = Object.fromEntries(Object.entries(data)
+ .map(([key, value]) => {
+ const newKey = key.split(".");
+ return [newKey[newKey.length - 1], value];
+ }));
+
+ const key = `${this.document.parent.id}_${this.document.documentName}_${this.document.id}`;
+ return game.user.update({
+ [`${CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG}.${key}`]: deconstructedData
+ });
+ }
+
+ async queueState(newState) {
+ const updates = {
+ [CONSTANTS.QUEUED_STATE_FLAG]: newState
+ };
+ if (Hooks.call("ats.preUpdateQueuedState", this.document, this.flags.data, updates) === false) {
+ return;
+ }
+ return this.update(updates);
+ }
+
+ async updateState(stateIndex) {
+ let previousStateIndex = this.flags.currentStateIndex;
+ if (this.flags.states[stateIndex].behavior === CONSTANTS.BEHAVIORS.RANDOM_STATE) {
+ previousStateIndex = stateIndex;
+ stateIndex = this.flags.determineNextStateIndex(stateIndex);
+ }
+ const updates = {
+ [CONSTANTS.UPDATED_FLAG]: Number(Date.now()),
+ [CONSTANTS.PREVIOUS_STATE_FLAG]: previousStateIndex,
+ [CONSTANTS.CURRENT_STATE_FLAG]: stateIndex,
+ [CONSTANTS.QUEUED_STATE_FLAG]: this.flags.determineNextStateIndex(stateIndex),
+ "texture.src": this.flags.determineFile(stateIndex)
+ };
+ if (Hooks.call("ats.preUpdateCurrentState", this.document, this.flags.data, updates) === false) {
+ return;
+ }
+ return this.update(updates);
+ }
+
+ async changeState({ state = null, step = 1, fast = false } = {}) {
+
+ if (this.nextButton) {
+ this.nextButton.removeClass("active");
+ }
+ if (this.prevButton) {
+ this.prevButton.removeClass("active");
+ }
+
+ this.clearRandomTimers();
+
+ if (!fast && this.flags.currentState.behavior !== CONSTANTS.BEHAVIORS.STILL) {
+ if (this.nextButton && this.prevButton && state === null) {
+ this[step > 0 ? "nextButton" : "prevButton"].addClass("active");
+ }
+ return this.queueState(state ?? this.flags.currentStateIndex + step);
+ }
+
+ clearTimeout(this.timeout);
+ this.timeout = null;
+
+ const currentStateIndex = this.flags.currentStateIndex;
+
+ return this.updateState(state ?? currentStateIndex + step).then(() => {
+ if (currentStateIndex !== state) return;
+ SocketHandler.emit(SocketHandler.REPLAY_CURRENT_STATE, { uuid: this.uuid, userId: game.userId });
+ this.replayCurrentState();
+ });
+
+ }
+
+ setupRandomTimers() {
+
+ if (game.user !== currentDelegator) return;
+
+ if (!(
+ this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL
+ || this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN
+ || this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.LOOP
+ )) {
+ return false;
+ }
+
+ for (const stateIndex of this.flags.determineNextRandomStates()) {
+ if (this.randomTimers[stateIndex]) continue;
+ const state = this.flags.states[stateIndex];
+ const delayStart = Number(state.randomStart) * 1000;
+ const delayEnd = Number(state.randomEnd) * 1000;
+ const delay = lib.randomIntegerBetween(delayStart, delayEnd);
+ if (CONFIG.debug.kinemancer) ui.notifications.notify(`Next random state in ${delay / 1000}s`)
+ let timerId = null;
+ timerId = setTimeout(() => {
+ delete this.randomTimers[stateIndex];
+ if (this.flags.currentStateIsRandom) return;
+ if (this.flags.currentStateIsStill) {
+ this.updateState(stateIndex);
+ } else if (this.flags.currentStateIsLoop) {
+ this.queueState(stateIndex);
+ }
+ }, delay)
+ this.randomTimers[stateIndex] = timerId;
+ }
+
+ }
+
+ clearRandomTimers() {
+ Object.values(this.randomTimers).forEach(timerId => clearTimeout(timerId));
+ this.randomTimers = {};
+ }
+
+ determineStartTime(stateIndex) {
+
+ const currState = this.flags.states?.[stateIndex];
+ const currStart = lib.isRealNumber(currState?.start)
+ ? Number(currState?.start) * this.flags.durationMultiplier
+ : (currState?.start ?? 0);
+
+ switch (currStart) {
+
+ case CONSTANTS.START.START:
+ return 0;
+
+ case CONSTANTS.START.END:
+ return this.duration;
+
+ case CONSTANTS.START.MID:
+ return Math.floor(this.duration / 2);
- case CONSTANTS.START.PREV:
- return this.determineEndTime(stateIndex - 1);
+ case CONSTANTS.START.PREV:
+ return this.determineEndTime(stateIndex - 1);
- }
+ }
- return currStart;
- }
+ return currStart;
+ }
- determineEndTime(stateIndex) {
+ determineEndTime(stateIndex) {
- const currState = this.flags.states?.[stateIndex];
- const currEnd = lib.isRealNumber(currState?.end)
- ? Number(currState?.end) * this.flags.durationMultiplier
- : (currState?.end ?? this.duration);
+ const currState = this.flags.states?.[stateIndex];
+ const currEnd = lib.isRealNumber(currState?.end)
+ ? Number(currState?.end) * this.flags.durationMultiplier
+ : (currState?.end ?? this.duration);
- switch (currEnd) {
+ switch (currEnd) {
- case CONSTANTS.END.END:
- return this.duration;
+ case CONSTANTS.END.END:
+ return this.duration;
- case CONSTANTS.END.MID:
- return Math.floor(this.duration / 2);
+ case CONSTANTS.END.MID:
+ return Math.floor(this.duration / 2);
- case CONSTANTS.END.NEXT:
- return this.determineStartTime(stateIndex + 1);
+ case CONSTANTS.END.NEXT:
+ return this.determineStartTime(stateIndex + 1);
- }
+ }
- return currEnd;
+ return currEnd;
- }
+ }
- evaluateVisibility() {
- const hidden = this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN;
- if (this.document.object) this.document.object.renderable = !hidden || game.user.isGM;
- if (this.document.object.mesh) {
- this.document.object.mesh.renderable = !hidden || game.user.isGM;
- this.document.object.mesh.alpha = hidden ? (game.user.isGM ? 0.5 : 0.0) : this.document.alpha;
- }
- return hidden;
- }
+ evaluateVisibility() {
+ const hidden = this.flags.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN || !this.ready;
+ if (this.document.object) this.document.object.renderable = !hidden || game.user.isGM;
+ if (this.document.object.mesh) {
+ this.document.object.mesh.renderable = !hidden || game.user.isGM;
+ this.document.object.mesh.alpha = hidden ? (game.user.isGM ? 0.5 : 0.0) : this.document.alpha;
+ }
+ return hidden;
+ }
- replayCurrentState() {
- this.still = false;
- this.playing = false;
- this.ignoreDate = true;
- clearTimeout(this.timeout);
- this.play()
- }
+ replayCurrentState() {
+ this.still = false;
+ this.playing = false;
+ this.ignoreDate = true;
+ clearTimeout(this.timeout);
+ this.play()
+ }
- getVideoPlaybackState(options) {
+ getVideoPlaybackState(options) {
- if (!this.ready) {
- return {
- playing: false, loop: false, offset: 0
- };
- }
+ if (!this.ready) {
+ return {
+ playing: false, loop: false, offset: 0
+ };
+ }
- if (!this.flags?.states?.length || !this.document?.object) return;
+ if (!this.flags?.states?.length || !this.document?.object) return;
- const startTime = this.newCurrentTime ?? this.determineStartTime(this.flags.currentStateIndex);
- const endTime = this.determineEndTime(this.flags.currentStateIndex) ?? this.duration;
- this.newCurrentTime = null;
+ const startTime = this.newCurrentTime ?? this.determineStartTime(this.flags.currentStateIndex);
+ const endTime = this.determineEndTime(this.flags.currentStateIndex) ?? this.duration;
+ this.newCurrentTime = null;
- this.evaluateVisibility();
+ this.evaluateVisibility();
- this.still = false;
- this.playing = options.playing && this.document.autoplay;
- this.texture.update();
+ this.still = false;
+ this.playing = options.playing && this.document.autoplay;
+ this.texture.update();
- switch (this.flags.currentState.behavior) {
+ switch (this.flags.currentState.behavior) {
- case CONSTANTS.BEHAVIORS.STILL:
- case CONSTANTS.BEHAVIORS.STILL_HIDDEN:
- return this.handleStillBehavior(options, startTime);
+ case CONSTANTS.BEHAVIORS.STILL:
+ case CONSTANTS.BEHAVIORS.STILL_HIDDEN:
+ return this.handleStillBehavior(options, startTime);
- case CONSTANTS.BEHAVIORS.LOOP:
- return this.handleLoopBehavior(options, startTime, endTime);
+ case CONSTANTS.BEHAVIORS.LOOP:
+ return this.handleLoopBehavior(options, startTime, endTime);
- default:
- return this.handleOnceBehavior(options, startTime, endTime);
+ default:
+ return this.handleOnceBehavior(options, startTime, endTime);
- }
- }
+ }
+ }
- setTimeout(callback, waitDuration) {
- clearTimeout(this.timeout);
- this.timeout = setTimeout(() => {
- this.timeout = null;
- callback();
- }, Math.ceil(waitDuration));
- }
+ setTimeout(callback, waitDuration) {
+ clearTimeout(this.timeout);
+ this.timeout = setTimeout(() => {
+ this.timeout = null;
+ callback();
+ }, Math.ceil(waitDuration));
+ }
- async handleStillBehavior(options, startTime) {
+ async handleStillBehavior(options, startTime) {
- this.setupRandomTimers();
+ this.setupRandomTimers();
- this.still = true;
+ this.still = true;
- const fn = () => {
- this.video.removeEventListener("seeked", fn);
- this.texture.update();
- }
- this.video.addEventListener("seeked", fn);
+ const fn = () => {
+ this.video.removeEventListener("seeked", fn);
+ this.texture.update();
+ }
+ this.video.addEventListener("seeked", fn);
- await this.video.play();
- this.video.loop = false;
- this.video.currentTime = (startTime ?? 0) / 1000;
- this.video.pause()
+ await this.video.play();
+ this.video.loop = false;
+ this.video.currentTime = (startTime ?? 0) / 1000;
+ this.video.pause()
- return false;
- }
+ return false;
+ }
- handleLoopBehavior(options, startTime, endTime = 0) {
+ handleLoopBehavior(options, startTime, endTime = 0) {
- this.setupRandomTimers();
+ this.setupRandomTimers();
- let loopDuration = (endTime - startTime) + this.flags.singleFrameDuration;
+ let loopDuration = (endTime - startTime) + this.flags.singleFrameDuration;
- if ((startTime + loopDuration) > this.duration) {
- loopDuration = (this.duration - startTime);
- }
+ if ((startTime + loopDuration) > this.duration) {
+ loopDuration = (this.duration - startTime);
+ }
- const offsetLoopTime = ((this.offset ?? 0) % loopDuration) ?? 0;
- const offsetStartTime = (startTime + offsetLoopTime);
+ const offsetLoopTime = ((this.offset ?? 0) % loopDuration) ?? 0;
+ const offsetStartTime = (startTime + offsetLoopTime);
- if (startTime === 0 && loopDuration === this.duration && !this.flags.queuedStateIndexIsDifferent) {
- return {
- playing: options.playing && this.document.autoplay, loop: true, offset: offsetStartTime / 1000
- };
- }
+ if (startTime === 0 && loopDuration === this.duration && !this.flags.queuedStateIndexIsDifferent) {
+ return {
+ playing: options.playing && this.document.autoplay, loop: true, offset: offsetStartTime / 1000
+ };
+ }
- this.offset = 0;
+ this.offset = 0;
- this.setTimeout(() => {
- this.playing = false;
- if (this.flags.queuedStateIndexIsDifferent) {
- return this.updateState(this.flags.queuedStateIndex);
- }
- this.play();
- }, loopDuration - offsetLoopTime);
+ this.setTimeout(() => {
+ this.playing = false;
+ if (this.flags.queuedStateIndexIsDifferent) {
+ return this.updateState(this.flags.queuedStateIndex);
+ }
+ this.play();
+ }, loopDuration - offsetLoopTime);
- return {
- playing: options.playing && this.document.autoplay, loop: false, offset: offsetStartTime / 1000
- }
+ return {
+ playing: options.playing && this.document.autoplay, loop: false, offset: offsetStartTime / 1000
+ }
- }
+ }
- handleOnceBehavior(options, startTime, endTime) {
+ handleOnceBehavior(options, startTime, endTime) {
- this.clearRandomTimers();
+ this.clearRandomTimers();
- this.setTimeout(async () => {
- let queuedState = this.flags.queuedStateIndex;
- if (queuedState === null) {
- queuedState = this.flags.determineNextStateIndex();
- }
- this.playing = false;
- this.video.pause();
- if (!this.flags.currentStateIsOnceThenStill) {
- return this.updateState(queuedState);
- } else {
- this.still = true;
- }
- }, (endTime - startTime));
+ this.setTimeout(async () => {
+ let queuedState = this.flags.queuedStateIndex;
+ if (queuedState === null) {
+ queuedState = this.flags.determineNextStateIndex();
+ }
+ this.playing = false;
+ this.video.pause();
+ if (!this.flags.currentStateIsOnceThenStill) {
+ return this.updateState(queuedState);
+ } else {
+ this.still = true;
+ }
+ }, (endTime - startTime));
- this.offset = 0;
+ this.offset = 0;
- if (this.flags.currentStateIsOnceThenStill && Number(Date.now()) >= (this.flags.data.updated + endTime) && !this.ignoreDate) {
- this.playing = false;
- this.still = true;
- this.video.currentTime = endTime / 1000;
- this.video.pause();
- this.texture.update();
- return {
- playing: false, loop: false, offset: endTime / 1000
- };
- }
+ if (this.flags.currentStateIsOnceThenStill && Number(Date.now()) >= (this.flags.data.updated + endTime) && !this.ignoreDate) {
+ this.playing = false;
+ this.still = true;
+ this.video.currentTime = endTime / 1000;
+ this.video.pause();
+ this.texture.update();
+ return {
+ playing: false, loop: false, offset: endTime / 1000
+ };
+ }
- this.ignoreDate = false;
+ this.ignoreDate = false;
- return {
- playing: options.playing && this.document.autoplay, loop: false, offset: startTime / 1000
- }
+ return {
+ playing: options.playing && this.document.autoplay, loop: false, offset: startTime / 1000
+ }
- }
+ }
}
class Flags {
- constructor(doc) {
- this.doc = doc;
- this.uuid = doc.uuid;
- this.delegationUuid = this.uuid.split(".").slice(1).join("_");
- this._data = false;
- }
-
- get data() {
- if (!this._data) {
- this._data = this.getData();
- }
- return this._data;
- }
-
- get currentFile() {
- return this.doc.texture.src;
- }
-
- get baseFile() {
- return foundry.utils.getProperty(this.doc, CONSTANTS.BASE_FILE_FLAG) ?? this.currentFile;
- }
-
- get clientDropdown() {
- return foundry.utils.getProperty(this.doc, CONSTANTS.CLIENT_DROPDOWN) ?? false;
- }
-
- get folderPath() {
- return foundry.utils.getProperty(this.doc, CONSTANTS.FOLDER_PATH_FLAG) ?? lib.getFolder(this.baseFile);
- }
-
- get useFiles() {
- return this.data?.useFiles ?? false;
- }
-
- get states() {
- return this.data?.states ?? [];
- }
-
- get offset() {
- return (Number(Date.now()) - this.updated) - this.singleFrameDuration;
- }
-
- get updated() {
- return this.data?.updated ?? 0;
- }
-
- get previousState() {
- return this.states[this.previousStateIndex];
- }
-
- get previousStateIndex() {
- return Math.max(0, Math.min(this.data.previousState ?? this.currentStateIndex, this.data.states.length - 1));
- }
-
- get currentState() {
- return this.states[this.currentStateIndex];
- }
-
- get currentStateIsStill() {
- return this.currentState.behavior === CONSTANTS.BEHAVIORS.STILL || this.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN
- }
-
- get currentStateIsLoop() {
- return this.currentState.behavior === CONSTANTS.BEHAVIORS.LOOP
- }
-
- get currentStateIsRandom() {
- return this.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM || this.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM_IF
- }
-
- get currentStateIsOnceThenStill() {
- return this.currentState.behavior === CONSTANTS.BEHAVIORS.ONCE_STILL;
- }
-
- get currentStateIndex() {
- const defaultStateIndex = this.data.states.findIndex(state => state.default) ?? 0;
- return Math.max(0, Math.min(this.data.currentState ?? defaultStateIndex, this.data.states.length - 1));
- }
-
- get queuedState() {
- return this.states[this.queuedStateIndex];
- }
-
- get queuedStateIndex() {
- return this.data.queuedState > -1 ? this.data.queuedState : null;
- }
-
- get durationMultiplier() {
- switch (this.data?.numberType ?? CONSTANTS.NUMBER_TYPES.FRAMES) {
- case CONSTANTS.NUMBER_TYPES.MILLISECONDS:
- return 1;
- case CONSTANTS.NUMBER_TYPES.SECONDS:
- return 1000;
- case CONSTANTS.NUMBER_TYPES.FRAMES:
- return 1000 / this.fps;
- }
- }
-
- get fps() {
- return (this.data?.fps || 24);
- }
-
- get singleFrameDuration() {
- return (1000 / this.fps);
- }
-
- get queuedStateIndexIsDifferent() {
- return this.queuedStateIndex !== null && this.queuedStateIndex !== this.currentStateIndex;
- }
-
- getData() {
- const documentFlags = foundry.utils.getProperty(this.doc, CONSTANTS.FLAGS);
- if (currentDelegator && !currentDelegator.isGM) {
- const userFlags = foundry.utils.getProperty(currentDelegator, CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG + "." + this.delegationUuid);
- if (userFlags?.updated && documentFlags?.updated && userFlags?.updated > documentFlags?.updated) {
- return userFlags;
- }
- }
- return documentFlags;
- }
-
- copyData() {
- copiedData.set({
- [CONSTANTS.STATES_FLAG]: this.data.states,
- [CONSTANTS.NUMBER_TYPE_FLAG]: this.data.numberType,
- [CONSTANTS.FPS_FLAG]: this.data.fps,
- [CONSTANTS.CURRENT_STATE_FLAG]: this.currentStateIndex
- });
- ui.notifications.notify("The Kinemancer | Copied video state data")
- }
-
- pasteData() {
- const localCopyData = get(copiedData);
- if (!localCopyData) return;
- if (foundry.utils.isEmpty(localCopyData)) return;
- this.doc.update({
- ...foundry.utils.deepClone(localCopyData)
- });
- ui.notifications.notify("The Kinemancer | Pasted video state data")
- }
-
- updateData() {
- this._data = this.getData();
- }
-
- getStateById(id) {
- const index = this.states.findIndex(state => state.id === id);
- return index >= 0 ? index : false;
- }
-
- getStateIndexFromSteps(steps = 1) {
- return Math.max(0, Math.min(this.currentStateIndex + steps, this.data.states.length - 1));
- }
-
- determineNextRandomStates(stateIndex = null) {
-
- stateIndex ??= this.currentStateIndex;
-
- const state = this.states[stateIndex];
-
- const nextStates = this.states.filter(s => {
- return s.behavior === CONSTANTS.BEHAVIORS.RANDOM || (s.behavior === CONSTANTS.BEHAVIORS.RANDOM_IF && s.randomState === state.id);
- }).map(s => this.states.indexOf(s));
-
- if (nextStates.length) {
- return nextStates;
- }
-
- return [Math.max(0, Math.min(stateIndex, this.states.length - 1))];
-
- }
-
- determineNextStateIndex(stateIndex = null) {
-
- stateIndex ??= this.currentStateIndex;
-
- const state = this.states[stateIndex];
-
- const index = Math.max(0, Math.min(stateIndex, this.states.length - 1));
-
- const defaultIndex = this.states.findIndex(s => s.default);
-
- switch (state?.behavior) {
- case CONSTANTS.BEHAVIORS.ONCE_NEXT:
- return this.states[index + 1] ? index + 1 : defaultIndex;
-
- case CONSTANTS.BEHAVIORS.ONCE_PREVIOUS:
- return this.states[index - 1] ? index - 1 : defaultIndex;
-
- case CONSTANTS.BEHAVIORS.ONCE_STILL:
- case CONSTANTS.BEHAVIORS.ONCE_PREVIOUS_ACTIVE:
- case CONSTANTS.BEHAVIORS.RANDOM:
- return this.currentStateIndex;
+ constructor(doc) {
+ this.doc = doc;
+ this.uuid = doc.uuid;
+ this.delegationUuid = this.uuid.split(".").slice(1).join("_");
+ this._data = false;
+ }
+
+ get data() {
+ if (!this._data) {
+ this._data = this.getData();
+ }
+ return this._data;
+ }
+
+ get currentFile() {
+ return this.doc.texture.src;
+ }
+
+ get baseFile() {
+ return foundry.utils.getProperty(this.doc, CONSTANTS.BASE_FILE_FLAG) ?? this.currentFile;
+ }
+
+ get clientDropdown() {
+ return foundry.utils.getProperty(this.doc, CONSTANTS.CLIENT_DROPDOWN) ?? false;
+ }
+
+ get folderPath() {
+ return foundry.utils.getProperty(this.doc, CONSTANTS.FOLDER_PATH_FLAG) ?? lib.getFolder(this.baseFile);
+ }
+
+ get useFiles() {
+ return this.data?.useFiles ?? false;
+ }
+
+ get states() {
+ return this.data?.states ?? [];
+ }
+
+ get offset() {
+ return (Number(Date.now()) - this.updated) - this.singleFrameDuration;
+ }
+
+ get updated() {
+ return this.data?.updated ?? 0;
+ }
+
+ get previousState() {
+ return this.states[this.previousStateIndex];
+ }
+
+ get previousStateIndex() {
+ return Math.max(0, Math.min(this.data.previousState ?? this.currentStateIndex, this.data.states.length - 1));
+ }
+
+ get currentState() {
+ return this.states[this.currentStateIndex];
+ }
+
+ get currentStateIsStill() {
+ return this.currentState.behavior === CONSTANTS.BEHAVIORS.STILL || this.currentState.behavior === CONSTANTS.BEHAVIORS.STILL_HIDDEN
+ }
+
+ get currentStateIsLoop() {
+ return this.currentState.behavior === CONSTANTS.BEHAVIORS.LOOP
+ }
+
+ get currentStateIsRandom() {
+ return this.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM || this.currentState.behavior === CONSTANTS.BEHAVIORS.RANDOM_IF
+ }
+
+ get currentStateIsOnceThenStill() {
+ return this.currentState.behavior === CONSTANTS.BEHAVIORS.ONCE_STILL;
+ }
+
+ get currentStateIndex() {
+ const defaultStateIndex = this.data.states.findIndex(state => state.default) ?? 0;
+ return Math.max(0, Math.min(this.data.currentState ?? defaultStateIndex, this.data.states.length - 1));
+ }
+
+ get queuedState() {
+ return this.states[this.queuedStateIndex];
+ }
+
+ get queuedStateIndex() {
+ return this.data.queuedState > -1 ? this.data.queuedState : null;
+ }
+
+ get durationMultiplier() {
+ switch (this.data?.numberType ?? CONSTANTS.NUMBER_TYPES.FRAMES) {
+ case CONSTANTS.NUMBER_TYPES.MILLISECONDS:
+ return 1;
+ case CONSTANTS.NUMBER_TYPES.SECONDS:
+ return 1000;
+ case CONSTANTS.NUMBER_TYPES.FRAMES:
+ return 1000 / this.fps;
+ }
+ }
+
+ get fps() {
+ return (this.data?.fps || 24);
+ }
+
+ get singleFrameDuration() {
+ return (1000 / this.fps);
+ }
+
+ get queuedStateIndexIsDifferent() {
+ return this.queuedStateIndex !== null && this.queuedStateIndex !== this.currentStateIndex;
+ }
+
+ getData() {
+ const documentFlags = foundry.utils.getProperty(this.doc, CONSTANTS.FLAGS);
+ if (currentDelegator && !currentDelegator.isGM) {
+ const userFlags = foundry.utils.getProperty(currentDelegator, CONSTANTS.DELEGATED_STATEFUL_VIDEOS_FLAG + "." + this.delegationUuid);
+ if (userFlags?.updated && documentFlags?.updated && userFlags?.updated > documentFlags?.updated) {
+ return userFlags;
+ }
+ }
+ return documentFlags;
+ }
+
+ copyData() {
+ copiedData.set({
+ [CONSTANTS.STATES_FLAG]: this.data.states,
+ [CONSTANTS.NUMBER_TYPE_FLAG]: this.data.numberType,
+ [CONSTANTS.FPS_FLAG]: this.data.fps,
+ [CONSTANTS.CURRENT_STATE_FLAG]: this.currentStateIndex
+ });
+ ui.notifications.notify("The Kinemancer | Copied video state data")
+ }
+
+ pasteData() {
+ const localCopyData = get(copiedData);
+ if (!localCopyData) return;
+ if (foundry.utils.isEmpty(localCopyData)) return;
+ this.doc.update({
+ ...foundry.utils.deepClone(localCopyData)
+ });
+ ui.notifications.notify("The Kinemancer | Pasted video state data")
+ }
+
+ updateData() {
+ this._data = this.getData();
+ }
+
+ getStateById(id) {
+ const index = this.states.findIndex(state => state.id === id);
+ return index >= 0 ? index : false;
+ }
+
+ getStateIndexFromSteps(steps = 1) {
+ return Math.max(0, Math.min(this.currentStateIndex + steps, this.data.states.length - 1));
+ }
+
+ determineNextRandomStates(stateIndex = null) {
+
+ stateIndex ??= this.currentStateIndex;
+
+ const state = this.states[stateIndex];
+
+ const nextStates = this.states.filter(s => {
+ return s.behavior === CONSTANTS.BEHAVIORS.RANDOM || (s.behavior === CONSTANTS.BEHAVIORS.RANDOM_IF && s.randomState === state.id);
+ }).map(s => this.states.indexOf(s));
+
+ if (nextStates.length) {
+ return nextStates;
+ }
+
+ return [Math.max(0, Math.min(stateIndex, this.states.length - 1))];
+
+ }
+
+ determineNextStateIndex(stateIndex = null) {
+
+ stateIndex ??= this.currentStateIndex;
+
+ const state = this.states[stateIndex];
+
+ const index = Math.max(0, Math.min(stateIndex, this.states.length - 1));
+
+ const defaultIndex = this.states.findIndex(s => s.default);
+
+ switch (state?.behavior) {
+ case CONSTANTS.BEHAVIORS.ONCE_NEXT:
+ return this.states[index + 1] ? index + 1 : defaultIndex;
+
+ case CONSTANTS.BEHAVIORS.ONCE_PREVIOUS:
+ return this.states[index - 1] ? index - 1 : defaultIndex;
+
+ case CONSTANTS.BEHAVIORS.ONCE_STILL:
+ case CONSTANTS.BEHAVIORS.ONCE_PREVIOUS_ACTIVE:
+ case CONSTANTS.BEHAVIORS.RANDOM:
+ return this.currentStateIndex;
+
+ case CONSTANTS.BEHAVIORS.RANDOM_IF:
+ const nextSpecific = this.getStateById(state.randomState);
+ return nextSpecific >= 0 ? nextSpecific : defaultIndex;
- case CONSTANTS.BEHAVIORS.RANDOM_IF:
- const nextSpecific = this.getStateById(state.randomState);
- return nextSpecific >= 0 ? nextSpecific : defaultIndex;
+ case CONSTANTS.BEHAVIORS.ONCE_SPECIFIC:
+ const nextIndex = this.getStateById(state.nextState);
+ return nextIndex >= 0 ? nextIndex : defaultIndex;
- case CONSTANTS.BEHAVIORS.ONCE_SPECIFIC:
- const nextIndex = this.getStateById(state.nextState);
- return nextIndex >= 0 ? nextIndex : defaultIndex;
+ case CONSTANTS.BEHAVIORS.RANDOM_STATE:
+ const nextStates = state.randomState.map(id => this.states.findIndex(s => s.id === id)).filter(i => i > -1);
+ nextStates.splice(nextStates.indexOf(this.previousStateIndex), 1);
+ return nextStates.length ? lib.randomArrayElement(nextStates) : defaultIndex;
+ }
- case CONSTANTS.BEHAVIORS.RANDOM_STATE:
- const nextStates = state.randomState.map(id => this.states.findIndex(s => s.id === id)).filter(i => i > -1);
- return nextStates.length ? lib.randomArrayElement(nextStates) : defaultIndex;
- }
+ return index;
- return index;
+ }
- }
+ determineFile(stateIndex) {
- determineFile(stateIndex) {
+ const state = this.states[stateIndex];
+ if (this.useFiles && this.folderPath) {
+ let filePath = state.file
+ ? this.folderPath + "/" + state.file
+ : this.baseFile;
- const state = this.states[stateIndex];
- if (this.useFiles && this.folderPath) {
- let filePath = state.file
- ? this.folderPath + "/" + state.file
- : this.baseFile;
+ if (this.currentFile.includes("__")) {
+ const colorVariation = this.currentFile.split("__")[1].split(".")[0];
+ filePath = filePath.replace(".webm", `__${colorVariation}.webm`)
+ }
- if (this.currentFile.includes("__")) {
- const colorVariation = this.currentFile.split("__")[1].split(".")[0];
- filePath = filePath.replace(".webm", `__${colorVariation}.webm`)
- }
+ return filePath;
+ }
+ if (this.currentFile.includes("__")) {
+ return this.currentFile;
+ }
+ return this.baseFile;
- return filePath;
- }
- if (this.currentFile.includes("__")) {
- return this.currentFile;
- }
- return this.baseFile;
-
- }
+ }
}
diff --git a/src/module.js b/src/module.js
index c7ef01f..cc75d31 100644
--- a/src/module.js
+++ b/src/module.js
@@ -11,102 +11,103 @@ import registerFilePicker from "./filepicker.js";
Hooks.once('init', async function () {
- registerLibwrappers();
- registerFilePicker();
- Settings.initialize();
- SocketHandler.initialize();
- StatefulVideo.registerHooks();
-
- game.thekinemancer = {
- StatefulVideo,
- CONSTANTS,
- copiedData,
- lib
- };
+ registerLibwrappers();
+ registerFilePicker();
+ Settings.initialize();
+ SocketHandler.initialize();
+ StatefulVideo.registerHooks();
+
+ game.thekinemancer = {
+ StatefulVideo,
+ CONSTANTS,
+ copiedData,
+ lib
+ };
});
Hooks.on("changeSidebarTab", (app) => {
- const button = $("
");
- button.on("click", () => {
- DownloaderApp.show();
- });
- app.element.find("#settings-game").append(button)
+ const button = $("
");
+ button.on("click", () => {
+ DownloaderApp.show();
+ });
+ app.element.find("#settings-game").append(button)
});
Hooks.once('ready', async function () {
- setTimeout(() => {
- StatefulVideo.determineCurrentDelegator();
- }, 250);
-
- document.addEventListener("visibilitychange", function () {
- if (document.hidden) {
- StatefulVideo.getAll().forEach(statefulVideo => {
- statefulVideo.video.pause();
- });
- } else {
- StatefulVideo.getAll().forEach(statefulVideo => {
- statefulVideo.offset = Number(Date.now()) - statefulVideo.flags.updated;
- statefulVideo.play();
- });
- }
- });
-
- if (game.user.isGM) {
- for (const scene of Array.from(game.scenes)) {
- const updates = Array.from(scene.tiles).map(tile => {
- if (!foundry.utils.getProperty(tile, CONSTANTS.STATES_FLAG)?.length || foundry.utils.getProperty(tile, CONSTANTS.BASE_FILE_FLAG)) return false;
- return {
- _id: tile.id,
- [CONSTANTS.BASE_FILE_FLAG]: tile?.texture?.src,
- [CONSTANTS.FOLDER_PATH_FLAG]: lib.getFolder(tile?.texture?.src),
- }
- }).filter(Boolean);
- if (updates.length) {
- await scene.updateEmbeddedDocuments("Tile", updates);
- }
- }
- }
+ setTimeout(() => {
+ StatefulVideo.determineCurrentDelegator();
+ }, 250);
+
+ document.addEventListener("visibilitychange", function () {
+ if (document.hidden) {
+ StatefulVideo.getAll().forEach(statefulVideo => {
+ statefulVideo.video.pause();
+ });
+ } else {
+ StatefulVideo.getAll().forEach(statefulVideo => {
+ statefulVideo.offset = Number(Date.now()) - statefulVideo.flags.updated;
+ statefulVideo.play();
+ });
+ }
+ });
+
+ if (game.user.isGM) {
+ for (const scene of Array.from(game.scenes)) {
+ const updates = Array.from(scene.tiles).map(tile => {
+ if (!foundry.utils.getProperty(tile, CONSTANTS.STATES_FLAG)?.length || foundry.utils.getProperty(tile, CONSTANTS.BASE_FILE_FLAG)) return false;
+ return {
+ _id: tile.id,
+ [CONSTANTS.BASE_FILE_FLAG]: tile?.texture?.src,
+ [CONSTANTS.FOLDER_PATH_FLAG]: lib.getFolder(tile?.texture?.src),
+ }
+ }).filter(Boolean);
+ if (updates.length) {
+ await scene.updateEmbeddedDocuments("Tile", updates);
+ }
+ }
+ }
});
function registerLibwrappers() {
- libWrapper.register(CONSTANTS.MODULE_NAME, 'Tile.prototype._destroy', function (wrapped) {
- if (this.isVideo) {
- StatefulVideo.tearDown(this.document.uuid);
- }
- return wrapped();
- }, "MIXED");
-
- libWrapper.register(CONSTANTS.MODULE_NAME, 'Token.prototype._destroy', function (wrapped) {
- if (this.isVideo) {
- StatefulVideo.tearDown(this.document.uuid);
- }
- return wrapped();
- }, "MIXED");
-
- libWrapper.register(CONSTANTS.MODULE_NAME, 'VideoHelper.prototype.play', async function (wrapped, video, options) {
- const videoOptions = { playing: options?.playing ?? true };
- for (const statefulVideo of StatefulVideo.getAll().values()) {
- if (video === statefulVideo.video) {
- if (this.locked || statefulVideo.destroyed || (statefulVideo.playing && videoOptions.playing) || statefulVideo.still) {
- return;
- }
- if (window.document.hidden) return video.pause();
- const newOptions = statefulVideo.getVideoPlaybackState(videoOptions);
- if (!newOptions) return;
- return wrapped(video, newOptions);
- }
- }
- return wrapped(video, options);
- }, "MIXED");
-
- libWrapper.register(CONSTANTS.MODULE_NAME, 'VideoHelper.prototype._onFirstGesture', async function (wrapped, event) {
- Hooks.callAll("canvasFirstUserGesture");
- return wrapped(event);
- }, "MIXED");
+ libWrapper.register(CONSTANTS.MODULE_NAME, 'Tile.prototype._destroy', function (wrapped) {
+ if (this.isVideo) {
+ StatefulVideo.tearDown(this.document.uuid);
+ }
+ return wrapped();
+ }, "MIXED");
+
+ libWrapper.register(CONSTANTS.MODULE_NAME, 'Token.prototype._destroy', function (wrapped) {
+ if (this.isVideo) {
+ StatefulVideo.tearDown(this.document.uuid);
+ }
+ return wrapped();
+ }, "MIXED");
+
+ libWrapper.register(CONSTANTS.MODULE_NAME, 'VideoHelper.prototype.play', async function (wrapped, video, options) {
+ const videoOptions = { playing: options?.playing ?? true };
+ const statefulVideos = StatefulVideo.getAll().values();
+ for (const statefulVideo of statefulVideos) {
+ if (video === statefulVideo.video) {
+ if (this.locked || statefulVideo.destroyed || (statefulVideo.playing && videoOptions.playing) || statefulVideo.still) {
+ return;
+ }
+ if (window.document.hidden) return video.pause();
+ const newOptions = statefulVideo.getVideoPlaybackState(videoOptions);
+ if (!newOptions) return;
+ return wrapped(video, newOptions);
+ }
+ }
+ return wrapped(video, options);
+ }, "MIXED");
+
+ libWrapper.register(CONSTANTS.MODULE_NAME, 'VideoHelper.prototype._onFirstGesture', async function (wrapped, event) {
+ Hooks.callAll("canvasFirstUserGesture");
+ return wrapped(event);
+ }, "MIXED");
}