diff --git a/src/config-props.js b/src/config-props.js deleted file mode 100644 index 19fc1a4..0000000 --- a/src/config-props.js +++ /dev/null @@ -1,87 +0,0 @@ -module.exports = new Set([ - 'hlsjsProgressive', - '__abSendDomainToFeeds', - '_abZoomThumbnail', - 'advertising', - 'aboutlink', - 'abouttext', - 'aestoken', - 'allowFullscreen', - 'analytics', - 'androidhls', - 'aspectratio', - 'autoPause', - 'autostart', - 'base', - 'captions', - 'cast', - 'controls', - 'defaultBandwidthEstimate', - 'description', - 'displaydescription', - 'displayHeading', - 'displayPlaybackLabel', - 'displaytitle', - 'drm', - 'duration', - 'enableDefaultCaptions', - 'events', - 'file', - 'forceLocalizationDefaults', - 'fwassetid', - 'floating', - 'ga', - 'generateSEOMetadata', - 'height', - 'hlsjsConfig', - 'hlsjsdefault', - 'horizontalVolumeSlider', - 'image', - 'intl', - 'key', - 'listbar', - 'liveSyncDuration', - 'liveTimeout', - 'localization', - 'logo', - 'mediaid', - 'mute', - 'nextUpDisplay', - 'nextupoffset', - 'pad', - 'ph', - 'pid', - 'pipIcon', - 'playbackRateControls', - 'playbackRates', - 'playlist', - 'playlistIndex', - 'plugins', - 'preload', - 'qualityLabel', - 'qualityLabels', - 'recommendations', - 'related', - 'renderCaptionsNatively', - 'repeat', - 'safarihlsjs', - 'sdkplatform', - 'selectedBitrate', - 'setTimeEvents', - 'skin', - 'sharing', - 'sources', - 'stagevideo', - 'streamtype', - 'stretching', - 'title', - 'tracks', - 'type', - 'variations', - 'volume', - 'width', - 'withCredentials', - 'doNotTrack', - 'doNotTrackCookies', - 'images', -]); diff --git a/src/jwplayer-react.d.ts b/src/jwplayer-react.d.ts index fb96c66..651fd1e 100644 --- a/src/jwplayer-react.d.ts +++ b/src/jwplayer-react.d.ts @@ -1,86 +1,16 @@ import * as React from 'react'; /** - * Example: {"2500":"High","1000":"Medium"} + * Player setup options, passed through verbatim to `jwplayer().setup()`. + * The player's config surface evolves independently of this package, so keys + * are intentionally left open rather than enumerated here. See the official + * references for the supported options and their shapes — top-level options: + * https://docs.jwplayer.com/players/reference/setup-options + * and every nested config section (advertising, captions, casting, drm, + * floating, playlists, related, sharing, skin, ...): + * https://docs.jwplayer.com/players/reference/configuration-options-getting-started */ -type QualityLabels = Record; - -type Stretching = 'uniform' | 'exactfit' | 'fill' | 'none'; - -/** - * Width in pixels or percentage - */ -type Width = number | string; - -interface AppearanceConfig { - aspectratio?: string; - controls?: boolean; - displaydescription?: boolean; - displayHeading?: boolean; - displayPlaybackLabel?: boolean; - displaytitle?: boolean; - height?: number; - horizontalVolumeSlider?: boolean; - nextUpDisplay?: boolean; - qualityLabels?: QualityLabels; - renderCaptionsNatively?: boolean; - stretching?: Stretching; - width?: Width; -} - -type AutoStart = 'viewable'; - -/** - * A positive value is an offset from the start of the video. - * A negative value is an offset from the end of the video. - * This property can be defined either as a number (-10) or a percentage as a string ("-2%") - */ -type NextUpOffset = string | number; - -interface BehaviorConfig { - aboutlink?: string; - abouttext?: string; - allowFullscreen?: boolean; - autostart?: AutoStart; - defaultBandwidthEstimate?: number; - generateSEOMetadata?: boolean; - liveSyncDuration?: number; - mute?: boolean; - nextupoffset?: NextUpOffset; - pipIcon?: string; - playbackRateControls?: boolean; - playbackRates?: number[]; - playlistIndex?: number; - repeat?: boolean; -} - -type MediaType = - | 'aac' | 'f4a' | 'f4v' | 'hls' | 'm3u' | 'm4v' | 'mov' | 'mp3' - | 'mp4' | 'mpeg' | 'oga' | 'ogg' | 'ogv' | 'vorbis' | 'webm'; - -interface MediaConfig { - file?: string; - description?: string; - image?: string; - mediaid?: string; - playlist?: string | object[]; - title?: string; - type?: MediaType; -} - -type Preload = 'metadata' | 'auto' | 'none'; - -interface RenderAndLoadingConfig { - base?: string; - flashplayer?: string; - hlsjsdefault?: boolean; - liveTimeout?: number; - loadAndParseHlsMetadata?: boolean; - preload?: Preload; -} - -export type JWPlayerConfig = - AppearanceConfig & BehaviorConfig & MediaConfig & RenderAndLoadingConfig; +export type JWPlayerConfig = Record; export type EventCallback = (...args: unknown[]) => void; @@ -94,7 +24,7 @@ export interface JWPlayerApi { once(event: string, callback: EventCallback): JWPlayerApi; off(event?: string, callback?: EventCallback): JWPlayerApi; remove(): void; - setup(config: Record): JWPlayerApi; + setup(config: JWPlayerConfig | object): JWPlayerApi; [member: string]: unknown; } @@ -109,13 +39,33 @@ export interface UnmountCallbackArguments { player: JWPlayerApi | null; } +/** + * Any prop that is not part of the component's own API (the declared props + * below and on/once handlers) is forwarded verbatim into + * `jwplayer().setup()`, so all player config options work as top-level props. + * + * Exception: React reserves the `key` prop and never forwards it, so a player + * license key only works via `config={{ key: ... }}`. + */ export interface JWPlayerProps extends JWPlayerConfig { didMountCallback?: (args: MountCallbackArguments) => void; willUnmountCallback?: (args: UnmountCallbackArguments) => void; id?: string; /** Required unless a jwplayer library script is already loaded on the page */ library?: string; - config?: JWPlayerConfig; + /** + * The `| object` arm accepts interface-typed configs, which lack the + * implicit index signature TypeScript requires for Record assignability. + */ + config?: JWPlayerConfig | object; + /** + * Config merging is shallow, so a top-level advertising prop replaces the + * entire advertising block — both `config.advertising` and the player + * library/dashboard defaults. Include the full ad config here, not just + * the keys being changed. See the official reference for its shape: + * https://docs.jwplayer.com/players/reference/advertising-config-ref + */ + advertising?: JWPlayerConfig | object; /** * on props subscribe to player events; onAll fires for every event. * once props subscribe to the first firing only. diff --git a/src/util.js b/src/util.js index b42a016..f552a5d 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,16 @@ -import configProps from './config-props'; +import { ON_REGEX } from './const'; + +// Props that configure this component rather than the player. Everything else +// is forwarded verbatim into setup(), so the player — not this wrapper — +// decides which config keys are meaningful. +const componentProps = new Set([ + 'children', + 'config', + 'didMountCallback', + 'id', + 'library', + 'willUnmountCallback', +]); let idIndex = -1; export function generateUniqueId() { @@ -55,7 +67,10 @@ export function generateConfig(props) { const config = {}; Object.keys(props).forEach((key) => { - if (configProps.has(key)) config[key] = props[key]; + if (componentProps.has(key)) return; + // on* event handler props (once* also matches ON_REGEX) are not config + if (key.match(ON_REGEX)) return; + config[key] = props[key]; }); return { ...props.config, ...config, isReactComponent: true }; diff --git a/test/jwplayer-react.test.js b/test/jwplayer-react.test.js index e90795a..a78c314 100644 --- a/test/jwplayer-react.test.js +++ b/test/jwplayer-react.test.js @@ -91,10 +91,11 @@ describe('methods', () => { }); describe('generateConfig', () => { - it('generates a setup config from props without assigning unsupported properties', async () => { - const { instance } = await createMountedComponent({ unsupportedProperty: 3, floating: {}, width: 500 }); + it('forwards all non-component props into the setup config, including custom keys', async () => { + const { instance } = await createMountedComponent({ _customAdServerData: { segment: 'sports' }, floating: {}, width: 500 }); const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; const expectedSetupConfig = { + _customAdServerData: { segment: 'sports' }, floating: {}, isReactComponent: true, playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', @@ -103,11 +104,28 @@ describe('methods', () => { expect(setupConfig).toEqual(expectedSetupConfig); }); + it('excludes component props and event handlers from the setup config', async () => { + const { instance } = await createMountedComponent({ + didMountCallback: noop, + willUnmountCallback: noop, + onPlay: noop, + onceReady: noop, + width: 500 + }); + const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; + expect(setupConfig).toEqual({ + isReactComponent: true, + playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', + width: 500 + }); + }); + it('Props overwrite matching base config properties', async () => { const baseConfig = { width: 400, height: 300 }; - const { instance } = await createMountedComponent({ config: baseConfig, unsupportedProperty: 3, floating: {}, width: 500 }); + const { instance } = await createMountedComponent({ config: baseConfig, _customAdServerData: { segment: 'sports' }, floating: {}, width: 500 }); const setupConfig = window.jwplayer(instance.id).setup.mock.calls[0][0]; const expectedSetupConfig = { + _customAdServerData: { segment: 'sports' }, floating: {}, isReactComponent: true, playlist: 'https://cdn.jwplayer.com/v2/media/1g8jjku3', diff --git a/test/types/consumer.tsx b/test/types/consumer.tsx index fe15456..cea23aa 100644 --- a/test/types/consumer.tsx +++ b/test/types/consumer.tsx @@ -6,6 +6,7 @@ */ import * as React from 'react'; import JWPlayer, { + JWPlayerConfig, JWPlayerInstance, JWPlayerProps, MountCallbackArguments, @@ -43,6 +44,71 @@ export const withInlineProps = ( /> ); +// generateConfig() spreads `config` verbatim into setup(), so custom keys the +// player config reference doesn't document must typecheck, including as an +// inline literal (excess-property checking would otherwise reject them). +export const withCustomConfigData = ( + +); + +// Any non-component prop is forwarded into setup(), so all player config +// options — and custom keys like _customAdServerData — work as top-level +// props with no dedicated declaration. +export const withTopLevelConfigProps = ( + +); + +// Interface types get no implicit index signature, so `config` and +// `advertising` need their `| object` arm to accept interface-typed values. +interface AppPlayerConfig { + file: string; + autostart: boolean; +} +declare const appConfig: AppPlayerConfig; +export const withInterfaceTypedConfig = ( + { player.setup(appConfig); }} + /> +); + +export const withCustomTopLevelProp = ( + +); + +// Declared props keep their strict types despite the open index signature. +// @ts-expect-error library must be a string +export const withBadLibrary = ; +// @ts-expect-error config must be an object +export const withBadConfig = ; +// @ts-expect-error advertising must be an object +export const withBadAdvertising = ; +// @ts-expect-error on props must be functions +export const withBadEventProp = ; + +export const customConfig: JWPlayerConfig = { + playlist: 'https://cdn.jwplayer.com/v2/media/abcd1234', + _customAdServerData: { segment: 'sports' }, +}; + // A ref resolves to the mounted instance, exposing the player API directly. const playerRef = React.createRef();