diff --git a/packages/core/media-store/src/factory.ts b/packages/core/media-store/src/factory.ts index d5917dad..69c696d7 100644 --- a/packages/core/media-store/src/factory.ts +++ b/packages/core/media-store/src/factory.ts @@ -42,6 +42,9 @@ export type StateMediator = { muted: FacadeProp; volume: FacadeProp; volumeLevel: ReadonlyFacadeProp<'high' | 'medium' | 'low' | 'off'>; + currentTime: FacadeProp; + duration: ReadonlyFacadeProp; + seekable: ReadonlyFacadeProp<[number, number] | undefined>; }; export function createMediaStore({ diff --git a/packages/core/media-store/src/index.ts b/packages/core/media-store/src/index.ts index b6ca3fea..fa3db319 100644 --- a/packages/core/media-store/src/index.ts +++ b/packages/core/media-store/src/index.ts @@ -2,5 +2,6 @@ export * from './factory'; export { createMediaStore } from './media-store'; export * from './state-mediators/playable'; export * from './state-mediators/audible'; +export * from './state-mediators/temporal'; export * from './component-state-definitions/play-button'; export * from './component-state-definitions/mute-button'; \ No newline at end of file diff --git a/packages/core/media-store/src/media-store.ts b/packages/core/media-store/src/media-store.ts index 5fb07826..f3f26347 100644 --- a/packages/core/media-store/src/media-store.ts +++ b/packages/core/media-store/src/media-store.ts @@ -1,9 +1,10 @@ import { playable } from './state-mediators/playable'; import { audible } from './state-mediators/audible'; +import { temporal } from './state-mediators/temporal'; import { createMediaStore as factory } from './factory'; // Example of default media store with default state mediator definitions. (CJP) // NOTE: We can also change the API to take an array of stateMediators (or either/both) (CJP) -const stateMediator = { ...playable, ...audible }; +const stateMediator = { ...playable, ...audible, ...temporal }; type Params = Partial[0]>; export const createMediaStore = (params: Params = {}) => factory({ stateMediator, ...params }); \ No newline at end of file diff --git a/packages/core/media-store/src/state-mediators/temporal.ts b/packages/core/media-store/src/state-mediators/temporal.ts new file mode 100644 index 00000000..ebdc3122 --- /dev/null +++ b/packages/core/media-store/src/state-mediators/temporal.ts @@ -0,0 +1,58 @@ +const isValidNumber = (value: any): value is number => { + return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value); +}; + +export const temporal = { + currentTime: { + get(stateOwners: any) { + const { media } = stateOwners; + return media?.currentTime ?? 0; + }, + set(value: number, stateOwners: any) { + const { media } = stateOwners; + if (!media || !isValidNumber(value)) return; + media.currentTime = value; + }, + mediaEvents: ['timeupdate', 'loadedmetadata'], + actions: { + /** @TODO Support more sophisticated seeking patterns like seek-to-live, relative seeking, etc. (CJP) */ + seekrequest: ( + { detail }: Pick, 'detail'> = { detail: 0 }, + ) => +detail, + }, + }, + + duration: { + get(stateOwners: any) { + const { media } = stateOwners; + + // Return 0 if no media or invalid duration + if (!media?.duration || Number.isNaN(media.duration) || !Number.isFinite(media.duration)) { + return 0; + } + + return media.duration; + }, + mediaEvents: ['loadedmetadata', 'durationchange', 'emptied'], + }, + + seekable: { + get(stateOwners: any): [number, number] | undefined { + const { media } = stateOwners; + + if (!media?.seekable?.length) return undefined; + + const start = media.seekable.start(0); + const end = media.seekable.end(media.seekable.length - 1); + + // Account for cases where metadata has an "empty" seekable range + if (!start && !end) return undefined; + + return [ + Number(start.toFixed(3)), + Number(end.toFixed(3)) + ]; + }, + mediaEvents: ['loadedmetadata', 'emptied', 'progress', 'seekablechange'], + }, +}; \ No newline at end of file