From f0f9eed544e668c67fbeb80aff39e818e905c3e9 Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Thu, 18 Jun 2026 15:00:26 -0700 Subject: [PATCH] refactor(core)!: move media capability predicates to core layer (#1705) Co-authored-by: Rahim --- packages/core/package.json | 5 +++++ packages/core/src/core/index.ts | 2 ++ .../core/src/{dom => core}/media/constants.ts | 2 +- .../core/src/{dom => core}/media/predicate.ts | 18 ++++++++++++------ .../media/tests/constants.test.ts | 0 .../media/tests/predicate.test.ts | 0 packages/core/src/dom/index.ts | 1 - packages/core/src/dom/media/media-host.ts | 2 +- packages/core/src/dom/media/vimeo/index.ts | 2 +- packages/core/src/dom/store/features/buffer.ts | 3 +-- .../core/src/dom/store/features/controls.ts | 3 +-- packages/core/src/dom/store/features/error.ts | 3 +-- packages/core/src/dom/store/features/live.ts | 3 +-- .../src/dom/store/features/playback-rate.ts | 3 +-- .../core/src/dom/store/features/playback.ts | 3 +-- .../core/src/dom/store/features/quality.ts | 3 +-- .../src/dom/store/features/remote-playback.ts | 3 +-- packages/core/src/dom/store/features/source.ts | 3 +-- .../core/src/dom/store/features/stream-type.ts | 3 +-- .../core/src/dom/store/features/text-track.ts | 5 ++--- packages/core/src/dom/store/features/time.ts | 7 ++++++- packages/core/src/dom/store/features/volume.ts | 2 +- packages/core/tsdown.config.ts | 1 + packages/html/src/index.ts | 3 ++- packages/react/src/index.ts | 2 ++ 25 files changed, 46 insertions(+), 36 deletions(-) rename packages/core/src/{dom => core}/media/constants.ts (94%) rename packages/core/src/{dom => core}/media/predicate.ts (90%) rename packages/core/src/{dom => core}/media/tests/constants.test.ts (100%) rename packages/core/src/{dom => core}/media/tests/predicate.test.ts (100%) diff --git a/packages/core/package.json b/packages/core/package.json index 6795fc90..9f272a76 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,6 +34,11 @@ "development": "./dist/dev/dom/media/*/index.js", "default": "./dist/default/dom/media/*/index.js" }, + "./media/predicate": { + "types": "./dist/dev/media/predicate.d.ts", + "development": "./dist/dev/media/predicate.js", + "default": "./dist/default/media/predicate.js" + }, "./i18n": { "types": "./dist/dev/i18n.d.ts", "development": "./dist/dev/i18n.js", diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts index 6f2d8316..865d3079 100644 --- a/packages/core/src/core/index.ts +++ b/packages/core/src/core/index.ts @@ -1,3 +1,5 @@ +export { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './media/constants'; +export * from './media/predicate'; export * from './media/state'; export * from './media/types'; export * from './ui/airplay-button/airplay-button-core'; diff --git a/packages/core/src/dom/media/constants.ts b/packages/core/src/core/media/constants.ts similarity index 94% rename from packages/core/src/dom/media/constants.ts rename to packages/core/src/core/media/constants.ts index cb92e46a..2d432966 100644 --- a/packages/core/src/dom/media/constants.ts +++ b/packages/core/src/core/media/constants.ts @@ -1,4 +1,4 @@ -import type { RemotePlaybackLike, TextTrackListLike, TimeRangeLike } from '../../core/media/types'; +import type { RemotePlaybackLike, TextTrackListLike, TimeRangeLike } from './types'; /** A frozen, empty `TimeRanges`-like value for hosts with no ranges. */ export const EMPTY_TIME_RANGES: TimeRangeLike = Object.freeze({ diff --git a/packages/core/src/dom/media/predicate.ts b/packages/core/src/core/media/predicate.ts similarity index 90% rename from packages/core/src/dom/media/predicate.ts rename to packages/core/src/core/media/predicate.ts index f8244118..49b24bd1 100644 --- a/packages/core/src/dom/media/predicate.ts +++ b/packages/core/src/core/media/predicate.ts @@ -1,5 +1,6 @@ import { isFunction, isObject, isUndefined } from '@videojs/utils/predicate'; +import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './constants'; import type { MediaBufferCapability, MediaErrorCapability, @@ -14,8 +15,7 @@ import type { MediaVideoDimensionsCapability, MediaVideoRenditionCapability, MediaVolumeCapability, -} from '../../core/media/types'; -import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './constants'; +} from './types'; export function hasMetadata(media: MediaSourceCapability): boolean { return media.readyState >= 1; @@ -109,11 +109,17 @@ export function isMediaLiveCapable(value: unknown): value is MediaLiveCapability return !isUndefined(media.liveEdgeStart) && !isUndefined(media.targetLiveWindow); } -export function isQuerySelectorAllCapable( +/** Framework-agnostic `NodeList`-like shape returned by `querySelectorAll`. */ +export interface NodeListLike { + readonly length: number; + readonly [index: number]: Element; + item(index: number): Element | null; + [Symbol.iterator](): Iterator; +} + +export function isQuerySelectorAllCapable( value: unknown -): value is { - querySelectorAll: (selectors: T) => NodeListOf]>; -} { +): value is { querySelectorAll: (selectors: string) => NodeListLike } { return ( isObject(value) && 'querySelectorAll' in value && isFunction((value as Record).querySelectorAll) ); diff --git a/packages/core/src/dom/media/tests/constants.test.ts b/packages/core/src/core/media/tests/constants.test.ts similarity index 100% rename from packages/core/src/dom/media/tests/constants.test.ts rename to packages/core/src/core/media/tests/constants.test.ts diff --git a/packages/core/src/dom/media/tests/predicate.test.ts b/packages/core/src/core/media/tests/predicate.test.ts similarity index 100% rename from packages/core/src/dom/media/tests/predicate.test.ts rename to packages/core/src/core/media/tests/predicate.test.ts diff --git a/packages/core/src/dom/index.ts b/packages/core/src/dom/index.ts index 9a22a97d..1b4e4de3 100644 --- a/packages/core/src/dom/index.ts +++ b/packages/core/src/dom/index.ts @@ -8,7 +8,6 @@ export * from './hotkey/aria'; export * from './hotkey/coordinator'; export * from './hotkey/hotkey'; export * from './hotkey/hotkey-events'; -export * from './media/predicate'; export * from './media/types'; export * from './store/features'; export * from './store/selectors'; diff --git a/packages/core/src/dom/media/media-host.ts b/packages/core/src/dom/media/media-host.ts index 709f61d0..f77a0d61 100644 --- a/packages/core/src/dom/media/media-host.ts +++ b/packages/core/src/dom/media/media-host.ts @@ -1,4 +1,5 @@ import type { EventListenerFor, EventType, QueriedElement } from '@videojs/utils/dom'; +import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from '../../core/media/constants'; import { type EventLike, type MediaFull, @@ -8,7 +9,6 @@ import { type TextTrackKind, type TextTrackLike, } from '../../core/media/types'; -import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './constants'; import { getComponents, getOwner, getProp, setProp } from './utils'; export { addComponent, getComponents, getOwner, getProp, setProp } from './utils'; diff --git a/packages/core/src/dom/media/vimeo/index.ts b/packages/core/src/dom/media/vimeo/index.ts index 3466fadb..76beae7f 100644 --- a/packages/core/src/dom/media/vimeo/index.ts +++ b/packages/core/src/dom/media/vimeo/index.ts @@ -1,7 +1,7 @@ import { isNull, isString, isUndefined } from '@videojs/utils/predicate'; import VimeoPlayer, { type LoadVideoOptions, type VimeoEmbedParameters, type VimeoUrl } from '@vimeo/player'; +import { EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from '../../../core/media/constants'; import type { ErrorLike, MediaPreloadType, TextTrackListLike, Video } from '../../../core/media/types'; -import { EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from '../constants'; import { MediaPlayedRangesMixin } from '../media-played-ranges'; export type { default as VimeoPlayerApi } from '@vimeo/player'; diff --git a/packages/core/src/dom/store/features/buffer.ts b/packages/core/src/dom/store/features/buffer.ts index 484c6113..7d132297 100644 --- a/packages/core/src/dom/store/features/buffer.ts +++ b/packages/core/src/dom/store/features/buffer.ts @@ -1,8 +1,7 @@ import { listen, serializeTimeRanges } from '@videojs/utils/dom'; - +import { isMediaBufferCapable } from '../../../core/media/predicate'; import type { MediaBufferState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaBufferCapable } from '../../media/predicate'; export const bufferFeature = definePlayerFeature({ name: 'buffer', diff --git a/packages/core/src/dom/store/features/controls.ts b/packages/core/src/dom/store/features/controls.ts index af1a6a46..79cd25be 100644 --- a/packages/core/src/dom/store/features/controls.ts +++ b/packages/core/src/dom/store/features/controls.ts @@ -1,10 +1,9 @@ import { listen } from '@videojs/utils/dom'; import { isNull } from '@videojs/utils/predicate'; - +import { isMediaPauseCapable, isMediaRemotePlaybackCapable } from '../../../core/media/predicate'; import type { MediaControlsState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; import { findGestureCoordinator } from '../../gesture/coordinator'; -import { isMediaPauseCapable, isMediaRemotePlaybackCapable } from '../../media/predicate'; import { isRemotePlaybackConnected, isRemotePlaybackConnecting } from '../../presentation/remote-playback'; const IDLE_DELAY = 2000; diff --git a/packages/core/src/dom/store/features/error.ts b/packages/core/src/dom/store/features/error.ts index ab1bb7ae..d608f8bf 100644 --- a/packages/core/src/dom/store/features/error.ts +++ b/packages/core/src/dom/store/features/error.ts @@ -1,8 +1,7 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaErrorCapable } from '../../../core/media/predicate'; import type { MediaErrorState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaErrorCapable } from '../../media/predicate'; export const errorFeature = definePlayerFeature({ name: 'error', diff --git a/packages/core/src/dom/store/features/live.ts b/packages/core/src/dom/store/features/live.ts index 4b6416cd..2a54ceba 100644 --- a/packages/core/src/dom/store/features/live.ts +++ b/packages/core/src/dom/store/features/live.ts @@ -1,8 +1,7 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaLiveCapable } from '../../../core/media/predicate'; import type { MediaLiveState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaLiveCapable } from '../../media/predicate'; /** * Player feature exposing `liveEdgeStart` and `targetLiveWindow` in store diff --git a/packages/core/src/dom/store/features/playback-rate.ts b/packages/core/src/dom/store/features/playback-rate.ts index 8e12caed..53b1a434 100644 --- a/packages/core/src/dom/store/features/playback-rate.ts +++ b/packages/core/src/dom/store/features/playback-rate.ts @@ -1,8 +1,7 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaPlaybackRateCapable } from '../../../core/media/predicate'; import type { MediaPlaybackRateState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaPlaybackRateCapable } from '../../media/predicate'; const DEFAULT_RATES: readonly number[] = [0.2, 0.5, 0.7, 1, 1.2, 1.5, 1.7, 2]; diff --git a/packages/core/src/dom/store/features/playback.ts b/packages/core/src/dom/store/features/playback.ts index ecb22ba9..4b6b6023 100644 --- a/packages/core/src/dom/store/features/playback.ts +++ b/packages/core/src/dom/store/features/playback.ts @@ -1,8 +1,7 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaPauseCapable, isMediaSeekCapable, isMediaSourceCapable } from '../../../core/media/predicate'; import type { MediaPlaybackState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaPauseCapable, isMediaSeekCapable, isMediaSourceCapable } from '../../media/predicate'; export const playbackFeature = definePlayerFeature({ name: 'playback', diff --git a/packages/core/src/dom/store/features/quality.ts b/packages/core/src/dom/store/features/quality.ts index 5d1f8b4e..e043dee5 100644 --- a/packages/core/src/dom/store/features/quality.ts +++ b/packages/core/src/dom/store/features/quality.ts @@ -1,9 +1,8 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaVideoDimensionsCapable, isMediaVideoRenditionCapable } from '../../../core/media/predicate'; import type { MediaQualityState, MediaVideoRendition } from '../../../core/media/state'; import type { VideoRenditionLike, VideoRenditionListLike } from '../../../core/media/types'; import { definePlayerFeature } from '../../feature'; -import { isMediaVideoDimensionsCapable, isMediaVideoRenditionCapable } from '../../media/predicate'; const QUALITY_AUTO_VALUE = 'auto'; diff --git a/packages/core/src/dom/store/features/remote-playback.ts b/packages/core/src/dom/store/features/remote-playback.ts index ff1367d6..9afe8d4a 100644 --- a/packages/core/src/dom/store/features/remote-playback.ts +++ b/packages/core/src/dom/store/features/remote-playback.ts @@ -1,8 +1,7 @@ import { isWebKitAirPlayCapable, listen, type WebkitAvailabilityEvent } from '@videojs/utils/dom'; - +import { isMediaRemotePlaybackCapable } from '../../../core/media/predicate'; import type { MediaRemotePlaybackState, RemotePlaybackConnectionState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaRemotePlaybackCapable } from '../../media/predicate'; import { exitFullscreen, isFullscreen } from '../../presentation/fullscreen'; import { isRemotePlaybackConnected, requestRemotePlayback } from '../../presentation/remote-playback'; diff --git a/packages/core/src/dom/store/features/source.ts b/packages/core/src/dom/store/features/source.ts index b911110a..c93d1f9d 100644 --- a/packages/core/src/dom/store/features/source.ts +++ b/packages/core/src/dom/store/features/source.ts @@ -1,8 +1,7 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaSourceCapable } from '../../../core/media/predicate'; import type { MediaSourceState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { isMediaSourceCapable } from '../../media/predicate'; export const sourceFeature = definePlayerFeature({ name: 'source', diff --git a/packages/core/src/dom/store/features/stream-type.ts b/packages/core/src/dom/store/features/stream-type.ts index d6b3334b..824a5f78 100644 --- a/packages/core/src/dom/store/features/stream-type.ts +++ b/packages/core/src/dom/store/features/stream-type.ts @@ -1,9 +1,8 @@ import { listen } from '@videojs/utils/dom'; - +import { isMediaBufferCapable, isMediaSeekCapable, isMediaStreamTypeCapable } from '../../../core/media/predicate'; import type { MediaStreamTypeState } from '../../../core/media/state'; import { type MediaStreamType, MediaStreamTypes } from '../../../core/media/types'; import { definePlayerFeature } from '../../feature'; -import { isMediaBufferCapable, isMediaSeekCapable, isMediaStreamTypeCapable } from '../../media/predicate'; export const streamTypeFeature = definePlayerFeature({ name: 'streamType', diff --git a/packages/core/src/dom/store/features/text-track.ts b/packages/core/src/dom/store/features/text-track.ts index e820d5e5..0d601b04 100644 --- a/packages/core/src/dom/store/features/text-track.ts +++ b/packages/core/src/dom/store/features/text-track.ts @@ -1,9 +1,8 @@ import { findTrackElement, getTextTrackList, isCaptionOrSubtitleTrack, listen } from '@videojs/utils/dom'; - +import { isMediaTextTrackCapable, isQuerySelectorAllCapable } from '../../../core/media/predicate'; import type { MediaTextCue, MediaTextTrack, MediaTextTrackState } from '../../../core/media/state'; import type { TextTrackLike } from '../../../core/media/types'; import { definePlayerFeature } from '../../feature'; -import { isMediaTextTrackCapable, isQuerySelectorAllCapable } from '../../media/predicate'; function getTrackId(track: TextTrackLike, index: number): string { return track.id || `track:${index}:${track.kind}:${track.language}:${track.label}`; @@ -111,7 +110,7 @@ export const textTrackFeature = definePlayerFeature({ // Listen for load events on tracks that don't have cues yet. // `addtrack` fires before cues are parsed — we need the `load` event // on the element to know when cues are ready. - const tracks = (isQuerySelectorAllCapable<'track'>(media) && media.querySelectorAll('track')) || []; + const tracks = (isQuerySelectorAllCapable(media) && media.querySelectorAll('track')) || []; const shadowTracks = (media instanceof HTMLElement && media.shadowRoot?.querySelectorAll('track')) || []; for (const trackEl of [...tracks, ...shadowTracks]) { diff --git a/packages/core/src/dom/store/features/time.ts b/packages/core/src/dom/store/features/time.ts index 95adb740..012df5cd 100644 --- a/packages/core/src/dom/store/features/time.ts +++ b/packages/core/src/dom/store/features/time.ts @@ -1,8 +1,13 @@ import { listen, onEvent } from '@videojs/utils/dom'; import { noop } from '@videojs/utils/function'; +import { + hasMetadata, + isMediaBufferCapable, + isMediaSeekCapable, + isMediaSourceCapable, +} from '../../../core/media/predicate'; import type { MediaTimeState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; -import { hasMetadata, isMediaBufferCapable, isMediaSeekCapable, isMediaSourceCapable } from '../../media/predicate'; import { signalKeys } from '../signal-keys'; export const timeFeature = definePlayerFeature({ diff --git a/packages/core/src/dom/store/features/volume.ts b/packages/core/src/dom/store/features/volume.ts index ea809492..c845bc19 100644 --- a/packages/core/src/dom/store/features/volume.ts +++ b/packages/core/src/dom/store/features/volume.ts @@ -1,8 +1,8 @@ import { listen } from '@videojs/utils/dom'; +import { isMediaVolumeCapable } from '../../../core/media/predicate'; import type { MediaVolumeState } from '../../../core/media/state'; import type { MediaFeatureAvailability } from '../../../core/media/types'; import { definePlayerFeature } from '../../feature'; -import { isMediaVolumeCapable } from '../../media/predicate'; /** Volume to restore when unmuting at zero. */ const UNMUTE_VOLUME = 0.25; diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index ab773b0a..1a63d1f0 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -14,6 +14,7 @@ const createConfig = (mode: PackageBuildMode): UserConfig => ({ ...packageBuildConfig(mode, 'neutral'), entry: { index: './src/core/index.ts', + 'media/predicate': './src/core/media/predicate.ts', i18n: './src/core/i18n/index.ts', ...localeEntries, dom: './src/dom/index.ts', diff --git a/packages/html/src/index.ts b/packages/html/src/index.ts index 3b56f82b..38791dd6 100644 --- a/packages/html/src/index.ts +++ b/packages/html/src/index.ts @@ -1,5 +1,7 @@ // Core export * from '@videojs/core/dom'; +// Media predicates +export * from '@videojs/core/media/predicate'; export type { Destroyable, PropertyDeclaration, @@ -10,7 +12,6 @@ export type { } from '@videojs/element'; // Element — reactive primitives for users extending MediaElement export { DestroyMixin, ReactiveElement } from '@videojs/element'; - // Store export type { Comparator, Selector } from '@videojs/store'; export { createSelector, shallowEqual } from '@videojs/store'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d04a0488..d99560ef 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -3,6 +3,8 @@ export type { IndicatorStatus, InputAction, InputIndicatorLabels } from '@videojs/core'; // Core export * from '@videojs/core/dom'; +// Media predicates +export * from '@videojs/core/media/predicate'; // Store export type { Comparator, Selector } from '@videojs/store'; export { createSelector, shallowEqual } from '@videojs/store';