@videojs/core
Version:
Core components and utilities for Video.js
451 lines (435 loc) • 14.2 kB
JavaScript
import { isValidNumber } from "@videojs/utils";
import { getKey, map, subscribeKeys } from "nanostores";
import { containsComposedNode } from "@videojs/utils/dom";
//#region src/store/definitions/current-time-display.ts
const currentTimeDisplayStateDefinition = {
keys: ["currentTime", "duration"],
stateTransform: (rawState) => {
const { currentTime, duration } = rawState;
return {
currentTime,
duration
};
},
createRequestMethods: (_dispatch) => ({})
};
//#endregion
//#region src/store/definitions/duration-display.ts
const durationDisplayStateDefinition = {
keys: ["duration"],
stateTransform: (rawState) => {
const { duration } = rawState;
return { duration };
},
createRequestMethods: (_dispatch) => ({})
};
//#endregion
//#region src/store/definitions/fullscreen-button.ts
const fullscreenButtonStateDefinition = {
keys: ["fullscreen"],
stateTransform: (rawState) => ({ fullscreen: rawState.fullscreen ?? false }),
createRequestMethods: (dispatch) => ({
requestEnterFullscreen: () => dispatch({
type: "fullscreenrequest",
detail: true
}),
requestExitFullscreen: () => dispatch({
type: "fullscreenrequest",
detail: false
})
})
};
//#endregion
//#region src/store/definitions/mute-button.ts
const muteButtonStateDefinition = {
keys: ["muted", "volumeLevel"],
stateTransform: (rawState) => ({
muted: rawState.muted ?? false,
volumeLevel: rawState.volumeLevel ?? "off"
}),
createRequestMethods: (dispatch) => ({
requestMute: () => dispatch({ type: "muterequest" }),
requestUnmute: () => dispatch({ type: "unmuterequest" })
})
};
//#endregion
//#region src/store/definitions/play-button.ts
const playButtonStateDefinition = {
keys: ["paused"],
stateTransform: (rawState) => ({ paused: rawState.paused ?? true }),
createRequestMethods: (dispatch) => ({
requestPlay: () => dispatch({ type: "playrequest" }),
requestPause: () => dispatch({ type: "pauserequest" })
})
};
//#endregion
//#region src/store/definitions/preview-time-display.ts
const previewTimeDisplayStateDefinition = {
keys: ["previewTime"],
stateTransform: (rawState) => ({ previewTime: rawState.previewTime ?? 0 })
};
//#endregion
//#region src/store/definitions/time-slider.ts
const timeSliderStateDefinition = {
keys: [
"currentTime",
"duration",
"previewTime"
],
stateTransform: (rawState) => ({
currentTime: rawState.currentTime ?? 0,
duration: rawState.duration ?? 0,
previewTime: rawState.previewTime ?? 0
}),
createRequestMethods: (dispatch) => ({
requestSeek: (time) => {
dispatch({
type: "seekrequest",
detail: time
});
},
requestPreview: (time) => {
dispatch({
type: "previewrequest",
detail: time
});
}
})
};
//#endregion
//#region src/store/definitions/volume-slider.ts
const volumeSliderStateDefinition = {
keys: [
"volume",
"muted",
"volumeLevel"
],
stateTransform: (rawState) => ({
volume: rawState.volume ?? 1,
muted: rawState.muted ?? false,
volumeLevel: rawState.volumeLevel ?? "high"
}),
createRequestMethods: (dispatch) => ({ requestVolumeChange: (volume) => {
dispatch({
type: "volumerequest",
detail: volume
});
} })
};
//#endregion
//#region src/store/factory.ts
function createMediaStore$1({ stateMediator: stateMediator$1 }) {
const stateOwners = {};
const store = map({});
const stateUpdateHandlerCleanups = {};
const keys = Object.keys(stateMediator$1);
function updateStateOwners(nextStateOwners) {
if (!Object.entries(nextStateOwners).some(([key, value]) => stateOwners[key] !== value)) return;
Object.entries(stateUpdateHandlerCleanups).forEach(([stateName, cleanups]) => {
cleanups.forEach((cleanup) => cleanup?.());
stateUpdateHandlerCleanups[stateName] = [];
});
Object.assign(stateOwners, nextStateOwners);
store.set(getInitialState(stateMediator$1, stateOwners));
Object.entries(stateMediator$1).forEach(([stateName, stateObject]) => {
const { get, stateOwnersUpdateHandlers = [] } = stateObject;
if (!stateUpdateHandlerCleanups[stateName]) stateUpdateHandlerCleanups[stateName] = [];
const updateHandler = (value) => {
const nextValue = value !== void 0 ? value : get(stateOwners);
store.setKey(stateName, nextValue);
};
stateOwnersUpdateHandlers.forEach((setupHandler) => {
const cleanup = setupHandler(updateHandler, stateOwners);
if (typeof cleanup === "function") stateUpdateHandlerCleanups[stateName]?.push(cleanup);
});
});
}
return {
dispatch(action) {
const { type, detail } = action;
if (type === "mediastateownerchangerequest") updateStateOwners({ media: detail });
else if (type === "containerstateownerchangerequest") updateStateOwners({ container: detail });
else Object.entries(stateMediator$1).forEach(([stateName, stateObject]) => {
const { set, actions } = stateObject;
if (actions?.[type]) {
const actionFn = actions[type];
const actionValue = actionFn(action);
if (set) set(actionValue, stateOwners);
else store.setKey(stateName, actionValue);
}
});
},
getState() {
return store.get();
},
getKeys(keys$1) {
return keys$1.reduce((acc, k) => {
acc[k] = getKey(store, k);
return acc;
}, {});
},
subscribeKeys(keys$1, callback) {
subscribeKeys(store, keys$1, callback);
},
subscribe(callback) {
subscribeKeys(store, keys, callback);
}
};
}
function getInitialState(stateMediator$1, stateOwners) {
const initialState = {};
for (const [stateName, { get }] of Object.entries(stateMediator$1)) {
if (!get) continue;
initialState[stateName] = get(stateOwners);
}
return initialState;
}
//#endregion
//#region src/store/mediators/audible.ts
const audible = {
muted: {
get(stateOwners) {
const { media } = stateOwners;
return media?.muted ?? false;
},
set(value, stateOwners) {
const { media } = stateOwners;
if (!media) return;
media.muted = value;
if (!value && !media.volume) media.volume = .25;
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
media.addEventListener("volumechange", eventHandler);
return () => media.removeEventListener("volumechange", eventHandler);
}],
actions: {
muterequest: () => true,
unmuterequest: () => false
}
},
volume: {
get(stateOwners) {
const { media } = stateOwners;
return media?.volume ?? 1;
},
set(value, stateOwners) {
const { media } = stateOwners;
if (!media) return;
const numericValue = +value;
if (!Number.isFinite(numericValue)) return;
media.volume = numericValue;
if (numericValue > 0) media.muted = false;
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
media.addEventListener("volumechange", eventHandler);
return () => media.removeEventListener("volumechange", eventHandler);
}],
actions: { volumerequest: ({ detail } = { detail: 0 }) => +detail }
},
volumeLevel: {
get(stateOwners) {
const { media } = stateOwners;
if (typeof media?.volume == "undefined") return "high";
if (media.muted || media.volume === 0) return "off";
if (media.volume < .5) return "low";
if (media.volume < .75) return "medium";
return "high";
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
media.addEventListener("volumechange", eventHandler);
return () => media.removeEventListener("volumechange", eventHandler);
}]
}
};
//#endregion
//#region src/store/mediators/fullscreenable.ts
/** @TODO This is implemented for web/browser only! We will need an alternative state mediator model for e.g. React Native. (CJP) */
const fullscreenable = { fullscreen: {
get(stateOwners) {
const { container } = stateOwners;
if (!container || !globalThis?.document) return false;
const doc = globalThis.document;
const currentFullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement;
if (!currentFullscreenElement) return false;
if (currentFullscreenElement === container) return true;
if (currentFullscreenElement.contains?.(container)) return true;
if (currentFullscreenElement.localName?.includes("-")) {
let currentRoot = currentFullscreenElement.shadowRoot;
const fullscreenElementKey = "fullscreenElement" in doc ? "fullscreenElement" : "webkitFullscreenElement" in doc ? "webkitFullscreenElement" : void 0;
if (fullscreenElementKey && !(fullscreenElementKey in (currentRoot || {}))) return containsComposedNode(currentFullscreenElement, container);
if (fullscreenElementKey) while (currentRoot?.[fullscreenElementKey]) {
if (currentRoot[fullscreenElementKey] === container) return true;
if (currentRoot[fullscreenElementKey]?.contains?.(container)) return true;
currentRoot = currentRoot[fullscreenElementKey]?.shadowRoot;
}
}
return false;
},
set(value, stateOwners) {
const { container } = stateOwners;
if (!container || !globalThis?.document) return;
try {
if (value) {
if (container.requestFullscreen) container.requestFullscreen();
else if (container.webkitRequestFullscreen) container.webkitRequestFullscreen();
else if (container.mozRequestFullScreen) container.mozRequestFullScreen();
else if (container.msRequestFullscreen) container.msRequestFullscreen();
} else {
const doc = globalThis.document;
if (doc.exitFullscreen) doc.exitFullscreen();
else if (doc.webkitExitFullscreen) doc.webkitExitFullscreen();
else if (doc.mozCancelFullScreen) doc.mozCancelFullScreen();
else if (doc.msExitFullscreen) doc.msExitFullscreen();
}
} catch (error) {
console.warn("Fullscreen operation failed:", error);
}
},
stateOwnersUpdateHandlers: [(handler, _stateOwners) => {
if (!globalThis?.document) return;
const eventHandler = () => handler();
const events = [
"fullscreenchange",
"webkitfullscreenchange",
"mozfullscreenchange",
"MSFullscreenChange"
];
events.forEach((event) => {
globalThis.document.addEventListener(event, eventHandler);
});
return () => {
events.forEach((event) => {
globalThis.document.removeEventListener(event, eventHandler);
});
};
}],
actions: { fullscreenrequest: ({ detail } = { detail: void 0 }) => {
if (typeof detail === "boolean") return detail;
return !globalThis?.document?.fullscreenElement;
} }
} };
//#endregion
//#region src/store/mediators/playable.ts
const playable = { paused: {
get(stateOwners) {
const { media } = stateOwners;
return media?.paused ?? true;
},
set(value, stateOwners) {
const { media } = stateOwners;
media?.[value ? "pause" : "play"]();
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
const events = [
"play",
"playing",
"pause",
"emptied"
];
events.forEach((event) => media.addEventListener(event, eventHandler));
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
}],
actions: {
playrequest: () => false,
pauserequest: () => true
}
} };
//#endregion
//#region src/store/mediators/preview.ts
const preview = { previewTime: { actions: { previewrequest: ({ detail } = { detail: 0 }) => +detail } } };
//#endregion
//#region src/store/mediators/temporal.ts
const temporal = {
currentTime: {
get(stateOwners) {
const { media } = stateOwners;
return media?.currentTime ?? 0;
},
set(value, stateOwners) {
const { media } = stateOwners;
if (!media || !isValidNumber(value)) return;
media.currentTime = value;
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
const events = ["timeupdate", "loadedmetadata"];
events.forEach((event) => media.addEventListener(event, eventHandler));
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
}],
actions: { seekrequest: ({ detail } = { detail: 0 }) => +detail }
},
duration: {
get(stateOwners) {
const { media } = stateOwners;
if (!media?.duration || Number.isNaN(media.duration) || !Number.isFinite(media.duration)) return 0;
return media.duration;
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
const events = [
"loadedmetadata",
"durationchange",
"emptied"
];
events.forEach((event) => media.addEventListener(event, eventHandler));
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
}]
},
seekable: {
get(stateOwners) {
const { media } = stateOwners;
if (!media?.seekable?.length) return void 0;
const start = media.seekable.start(0);
const end = media.seekable.end(media.seekable.length - 1);
if (!start && !end) return void 0;
return [Number(start.toFixed(3)), Number(end.toFixed(3))];
},
stateOwnersUpdateHandlers: [(handler, stateOwners) => {
const { media } = stateOwners;
if (!media) return;
const eventHandler = () => handler();
const events = [
"loadedmetadata",
"emptied",
"progress",
"seekablechange"
];
events.forEach((event) => media.addEventListener(event, eventHandler));
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
}]
}
};
//#endregion
//#region src/store/media-store.ts
const stateMediator = {
...playable,
...audible,
...temporal,
...fullscreenable,
...preview
};
function createMediaStore(params = {}) {
return createMediaStore$1({
stateMediator,
...params
});
}
//#endregion
export { audible, createMediaStore, currentTimeDisplayStateDefinition, durationDisplayStateDefinition, fullscreenButtonStateDefinition, muteButtonStateDefinition, playButtonStateDefinition, playable, previewTimeDisplayStateDefinition, temporal, timeSliderStateDefinition, volumeSliderStateDefinition };
//# sourceMappingURL=store.js.map