UNPKG

@videojs/core

Version:

Core components and utilities for Video.js

451 lines (435 loc) 14.2 kB
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