Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 0 additions & 87 deletions src/config-props.js

This file was deleted.

112 changes: 31 additions & 81 deletions src/jwplayer-react.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;

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<string, unknown>;

export type EventCallback = (...args: unknown[]) => void;

Expand All @@ -94,7 +24,7 @@ export interface JWPlayerApi {
once(event: string, callback: EventCallback): JWPlayerApi;
off(event?: string, callback?: EventCallback): JWPlayerApi;
remove(): void;
setup(config: Record<string, unknown>): JWPlayerApi;
setup(config: JWPlayerConfig | object): JWPlayerApi;
[member: string]: unknown;
}

Expand All @@ -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<Event>/once<Event> 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<Event> props subscribe to player events; onAll fires for every event.
* once<Event> props subscribe to the first firing only.
Expand Down
19 changes: 17 additions & 2 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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 };
Expand Down
24 changes: 21 additions & 3 deletions test/jwplayer-react.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
66 changes: 66 additions & 0 deletions test/types/consumer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import * as React from 'react';
import JWPlayer, {
JWPlayerConfig,
JWPlayerInstance,
JWPlayerProps,
MountCallbackArguments,
Expand Down Expand Up @@ -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 = (
<JWPlayer
library="https://cdn.jwplayer.com/libraries/abcd1234.js"
playlist="https://cdn.jwplayer.com/v2/media/abcd1234"
config={{ _customAdServerData: { segment: 'sports' } }}
/>
);

// 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 = (
<JWPlayer
library="https://cdn.jwplayer.com/libraries/abcd1234.js"
playlist="https://cdn.jwplayer.com/v2/media/abcd1234"
floating={{ dismissible: true }}
advertising={{
client: 'googima',
schedule: [{ offset: 'pre', tag: 'https://example.com/vast.xml' }],
_customAdServerData: { segment: 'sports' },
}}
/>
);

// 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 = (
<JWPlayer
library="https://cdn.jwplayer.com/libraries/abcd1234.js"
config={appConfig}
didMountCallback={({ player }) => { player.setup(appConfig); }}
/>
);

export const withCustomTopLevelProp = (
<JWPlayer
library="https://cdn.jwplayer.com/libraries/abcd1234.js"
playlist="https://cdn.jwplayer.com/v2/media/abcd1234"
_customAdServerData={{ segment: 'sports' }}
/>
);

// Declared props keep their strict types despite the open index signature.
// @ts-expect-error library must be a string
export const withBadLibrary = <JWPlayer library={123} />;
// @ts-expect-error config must be an object
export const withBadConfig = <JWPlayer config="nope" />;
// @ts-expect-error advertising must be an object
export const withBadAdvertising = <JWPlayer advertising="googima" />;
// @ts-expect-error on<Event> props must be functions
export const withBadEventProp = <JWPlayer onPlay="not-a-callback" />;

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<JWPlayerInstance>();

Expand Down
Loading