Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ These props are required to instantiate an instance of JW Player:
* Must be a url to a jwplayer web player library. Required if jwplayer library not already instantiated on page (ie. if window.jwplayer is undefined).
* Type: `string`
* Example: `https://content.jwplatform.com/libraries/abcd1234.js`
* If you load the library yourself — a `<script>` tag in your HTML, or a Next.js `<Script>` — you can omit this prop. The component then waits up to 10 seconds for `window.jwplayer` to appear, so a player that mounts before your script finishes still sets up. If it never appears, the component logs an error and creates no player.
<br>
* `playlist` OR `file` OR `advertising` block with `oustream: true`
* Player will require content in order to instantiate. See more [here](https://developer.jwplayer.com/jwplayer/docs/jw8-player-configuration-reference).
Expand Down
38 changes: 36 additions & 2 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,43 @@ export function createPlayerLoadPromise(url) {
// can be retried.
const loadPromises = new Map();

// How long to wait for a library the consumer loads themselves before giving up.
export const EXTERNAL_LIBRARY_TIMEOUT_MS = 10000;
const EXTERNAL_LIBRARY_POLL_MS = 50;

// window.jwplayer must be callable, not merely present: a library script that is
// still executing, or a placeholder left by another integration, would otherwise
// be mistaken for a ready player and fail inside setup().
function isLibraryReady() {
return typeof window.jwplayer === 'function';
}

// Without a library prop the consumer loads the script themselves — a Next.js
// <Script>, a tag in index.html — and that script can still be pending when a
// player mounts. Nothing fires an event when window.jwplayer is assigned, so
// poll for it instead of failing the mount outright.
function waitForExternalLibrary() {
return new Promise((resolve, reject) => {
const deadline = Date.now() + EXTERNAL_LIBRARY_TIMEOUT_MS;
const poll = setInterval(() => {
if (isLibraryReady()) {
clearInterval(poll);
resolve();
return;
}

if (Date.now() >= deadline) {
clearInterval(poll);
reject(new Error('jwplayer-react requires either a library prop, or a library script. Waited '
+ `${EXTERNAL_LIBRARY_TIMEOUT_MS}ms for window.jwplayer and it was never defined.`));
}
}, EXTERNAL_LIBRARY_POLL_MS);
});
}

export function loadPlayer(url) {
if (!window.jwplayer && !url) throw new Error('jwplayer-react requires either a library prop, or a library script');
if (window.jwplayer) return Promise.resolve();
if (isLibraryReady()) return Promise.resolve();
if (!url) return waitForExternalLibrary();

const pending = loadPromises.get(url);
if (pending && pending.script.isConnected) {
Expand Down
99 changes: 96 additions & 3 deletions test/jwplayer-react.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import React from 'react';
import { render, act } from '@testing-library/react';
import JWPlayer from '../src/jwplayer';
import { loadPlayer } from '../src/util';
import { loadPlayer, EXTERNAL_LIBRARY_TIMEOUT_MS } from '../src/util';
import { mockLibrary, players } from './util';

const noop = () => {};
Expand All @@ -21,6 +21,7 @@ afterEach(() => {
window.jwplayer = null;
document.querySelectorAll('script').forEach((script) => script.remove());
jest.restoreAllMocks();
jest.useRealTimers();
});

const createMountedComponent = async (props = {}) => {
Expand Down Expand Up @@ -63,9 +64,17 @@ describe('setup', () => {
checkTests();
});

it('Errors with no library and falsey window.jwplayer', () => {
// Used to throw synchronously. It now waits for a library the consumer loads
// themselves, so the same error arrives as a rejection after the timeout.
it('Errors with no library and falsey window.jwplayer', async () => {
jest.useFakeTimers();
window.jwplayer = null;
expect(() => loadPlayer()).toThrow("jwplayer-react requires either a library prop, or a library script");

const rejection = expect(loadPlayer()).rejects
.toThrow('jwplayer-react requires either a library prop, or a library script');
jest.advanceTimersByTime(EXTERNAL_LIBRARY_TIMEOUT_MS + 100);

await rejection;
});

it('creates a script tag when mounted if window.jwplayer is not defined', () => {
Expand Down Expand Up @@ -412,3 +421,87 @@ describe('methods', () => {
});
});
});

// A library the consumer loads themselves can still be pending at mount.
// See https://github.com/jwplayer/jwplayer-react/issues/12
describe('externally loaded library', () => {
it('waits for window.jwplayer and sets up once it appears', async () => {
jest.useFakeTimers();
window.jwplayer = null;
const ref = React.createRef();

// No library prop: the consumer's own script tag has not run yet.
await act(async () => {
render(<JWPlayer ref={ref} playlist={playlist} />);
});
expect(ref.current.player).toBe(null);

window.jwplayer = mockLibrary;
await act(async () => {
jest.advanceTimersByTime(100);
});

expect(ref.current.player).toBe(players[ref.current.id]);
});

it('logs and creates no player when the external library never appears', async () => {
jest.useFakeTimers();
window.jwplayer = null;
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const ref = React.createRef();

await act(async () => {
render(<JWPlayer ref={ref} playlist={playlist} />);
});

await act(async () => {
jest.advanceTimersByTime(EXTERNAL_LIBRARY_TIMEOUT_MS + 100);
});

expect(ref.current.player).toBe(null);
expect(errorSpy).toHaveBeenCalled();
});

// Asserted against loadPlayer directly so React's own scheduler timers do
// not count towards the total.
it('stops polling once the library resolves', async () => {
jest.useFakeTimers();
window.jwplayer = null;

const pending = loadPlayer();
expect(jest.getTimerCount()).toBe(1);

window.jwplayer = mockLibrary;
jest.advanceTimersByTime(100);
await pending;

expect(jest.getTimerCount()).toBe(0);
});

it('stops polling when it gives up', async () => {
jest.useFakeTimers();
window.jwplayer = null;

const rejection = expect(loadPlayer()).rejects.toThrow('never defined');
jest.advanceTimersByTime(EXTERNAL_LIBRARY_TIMEOUT_MS + 100);
await rejection;

expect(jest.getTimerCount()).toBe(0);
});

// The failure reported in the issue: window.jwplayer was present but not yet
// callable, so setup() blew up.
it('does not treat a non-callable window.jwplayer as ready', async () => {
window.jwplayer = { partiallyInitialized: true };
const ref = React.createRef();

await act(async () => {
render(<JWPlayer ref={ref} library={library} playlist={playlist} />);
});

const scripts = Array.from(document.getElementsByTagName('script'))
.filter((tag) => tag.src === library);
expect(scripts.length).toBe(1);
expect(ref.current.player).toBe(null);
});
});
Loading