Setting up the JW Player Plugin with WIREWAX embedder SDK (Fandom Setup)

Last update: 2021-12-17 5:50PM EST

important

WIREWAX embedder SDK Fandom build is for Fandom Setup use only, please don't share the link to this doc to any non-Fandom domains.

important

JW Player Custom Plugin is a JavaScript script loaded by API embed JW Player. Please note that the latest JW Player version that supports custom plugin is v8.23.1. On Dec 14, 2021 JW player released v8.24.0, which impacts the custom plugin loading process. We are awaiting for JW player's new guidance on registering custom plugin in v8.24.0, hence this guide is subject to change within the next a few weeks.

Brief#

This guide is for use of the WIREWAX embedder SDK to build a custom Fandom plugin for JW player. In this article, we will cover a minimal implementation of a JW custom plugin, and some additional methods to enable WIREWAX interactions.

Basics#

Here is a minimal use case of JW player API embed and the way to load a custom plugin:

<head>
<!-- JW Player Script -->
<script
type="text/javascript"
src="https://content.jwplatform.com/libraries/VXc5h4Tf.js "
></script>
</head>
<body>
<div class="container-wrapper">
<div id="jw-video-container"></div>
</div>
<script>
jwplayer("jw-video-container").setup({
playlist:
"https://cdn.jwplayer.com/v2/media/JW_MEDIA_ID" |
"https://cdn.jwplayer.com/v2/playlists/JW_PLAYLIST_ID",
plugins: { PATH_TO_JW_PLAYER_PLUGIN: {} }, // <- Load JW Player Custom Plugin script, such as "fandom-jw-plugin.js"
});
</script>
</body>

For events binding, we will need to pass the JW player instance to plugin:

jwplayer("jw-video-container")
.setup({
// JW setup options goes here
})
.on("ready", (event) => {
const customPlugin = new FandomPlugin("jw-video-container", {
player: jwplayer("jw-video-container"),
// Any additional options that's useful to configure plugin goes here, such as autoplay etc.
});
});

Then setup a new plugin script for implementation. The registering step is subject to change. Currently it will throw warning 305100. It doesn't impact the plugin at this moment (v8.23.1). Subject to update for latest v8.24.0 from JW Player.

// fandom-jw-plugin.js
class FandomPlugin {
constructor(rootId, options) {
this.rootId = rootId;
this.options = options;
this.player = options.player;
// ...
return this;
}
// Implementation
// ...
}
const registerPlugin =
window.jwplayerPluginJsonp || window.jwplayer().registerPlugin;
registerPlugin("FandomPlugin", "8.0", FandomPlugin);
export default FandomPlugin;

Step by step guidance#

The overall steps for creating a WIREWAX powered JW Player plugin are:

  1. Valid interaction
  2. Setup or inject WIREWAX embedder SDK script (SDK script can be appended in head directly, however, not recommend doing so without any validating process)
  3. Resolve JW media id to WIREWAX vidId
  4. Initialize WIREWAX embedder and create WIREWAX video-less player
  5. Binding events

Depending on the use cases and client side setup, step 1~3 can switch to a different order.

Fandom use case is mostly JW player playlist, the following example is based on playlist usage, for more details and JW player event, please find the details here at JW player JavaScript API documentation.

Step 1 - on JW "playlistItem" event, validate interaction#

Validate interaction is a step to make sure WIREWAX interaction exists in the current video, normally by checking if a JW media id is marked as interactivity enabled. Some clients has their own checking methods or endpoint to make sure the plugin won't load in non-interactive videos. In single interactive video or full interactive video playlist, this step is not necessary.

Since Fandom playlists are randomized mixed playlist, we highly recommend to validate if a playlist item has interaction first before injecting SDK.

An alternative way to validate interaction is to combine step 0 and step 2 together using the following utility method. If an error is thrown, we can assume this video is not interactive and skip:

/**
* resolve JW mediaId to WIREWAX vidId
*
* @param {string} JW media id string
* @return {string} WIREWAX vidId
*/
const fetchWIREWAXVidId = async (mediaid) => {
if (!mediaid) {
throw new TypeError("No JW media id is specified.");
}
const response = await fetch(
`https://edge-player.wirewax.com/jwPlayerData/${mediaid}.txt`
);
if (response.status !== 200) {
throw new Error("No vidId is mapped with this mediaid");
}
const vidId = await response.json();
return vidId;
};

Depending on playlist setup, JW media id can be found as mediaid or videoId:

// fandom-jw-plugin.js
class FandomPlugin {
constructor(rootId, options) {
this.rootId;
this.options = options;
this.player = options.player;
this.player.on("playlistItem", () => {
const mediaId =
this.player.getConfig().playlistItem.mediaid ||
this.player.getConfig().playlistItem.videoId;
// Combine validating and resolving id together
fetchWIREWAXVidId(mediaId)
.then((vidId) => {
this.vidId = vidId;
})
.catch((error) => {
console.warn(error);
});
});
return this;
}
// ...
}

Step 2 - Injecting embedder SDK on demand#

Next, once we have vidId resolved, we need to inject the embedder SDK through a utility function within the plugin script. Embedder SDK can be setup outside of the plugin script in the page head, as long as the JW player video on this page has interaction:

  • Fandom SDK url: https://edge-assets-wirewax.wikia-services.com/creativeData/sdk-fandom/wirewax-embedder-sdk.js
/**
* Inject WIREWAX embedder SDK script
*/
const injectEmbedderSDK = () => {
if (window.createWirewaxEmbedder) {
console.log("Embedder SDK is already loaded");
return;
}
const fandomSDKUrl =
"https://edge-assets-wirewax.wikia-services.com/creativeData/sdk-fandom/wirewax-embedder-sdk.js";
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = fandomSDKUrl;
script.addEventListener("load", resolve);
script.addEventListener("error", (e) => reject(e.error));
document.head.appendChild(script);
});
};
// fandom-jw-plugin.js
class FandomPlugin {
constructor(rootId, options) {
this.rootId;
this.options = options;
this.player = options.player;
this.player.on("playlistItem", () => {
const mediaId =
this.player.getConfig().playlistItem.mediaid ||
this.player.getConfig().playlistItem.videoId;
fetchWIREWAXVidId(mediaId)
.then((vidId) => {
this.vidId = vidId;
return injectEmbedderSDK(); // Injecting embedder SDK only when interaction is enabled in currently playlist item
})
.catch((error) => {
console.warn(error);
});
});
return this;
}
// ...
}

Step 3 - Initialize embedder and video-less WIREWAX player#

In this step, we will:

  • Create a container that matches JW Player media area to display WIREWAX interactive elements
  • Initialize WIREWAX embedder and create video-less WIREWAX player

Embedder only need to be initialized once:

// fandom-jw-plugin.js
class FandomPlugin {
// Constructor
// ...
async setupEmbedder() {
if (!this.embedder) {
// Create container
this.container = document.createElement("div");
this.container.classList.add("vjs-wirewax-container");
this.container.setAttribute(
"style",
"position: absolute; height: 100%; width: 100%; top: 0; pointer-events: none"
);
this.player.getContainer().appendChild(this.container);
// Initialize embedder
this.embedder = window.createWirewaxEmbedder();
}
// Initialize WIREWAX video-less player
this.embedder.createEl(this.container, {
isPlugin: true,
videoId: this.vidId,
rootId: this.rootId, // unique id to query JW player
});
return this.embedder.ready();
}
}

Step 4 - register time event to enable time sync in between WIREWAX player and JW Player#

WIREWAX powers client videos at frame level accuracy. Currently JW Player time event only supports as frequently as 10 times per second, which is below regular 24fps. In order to sync playback to frame level, a custom time update (timeline sync) handler need to set. There are a few ways to implement. In the following snippet, we will use requestAnimationFrame:

// fandom-jw-plugin.js
class FandomPlugin {
// Constructor
// ...
registerEvents() {
// Set up custom time handler
const HTML5VideoEl = this.player.getConfig().mediaElement;
this.setWIREWAXCurrentTime = () => {
this.embedder.setCurrentTime(HTML5VideoEl.currentTime);
this.animationId = window.requestAnimationFrame(
this.setWIREWAXCurrentTime
);
};
}
startTimeUpdate() {
window.cancelAnimationFrame(this.animationId);
this.animationId = window.requestAnimationFrame(this.setWIREWAXCurrentTime);
}
stopTimeUpdate() {
window.cancelAnimationFrame(this.animationId);
}
}

Step 5 - setup other event handlers#

Next, we will setup other playback or interaction event handlers for custom usage, since the JW Player instance doesn't change on playlistItem event, this only needs to be done once.

// fandom-jw-plugin.js
class FandomPlugin {
// Constructor
// ...
registerEvents() {
// ...
if (this.isPlayerRegistered) return;
// Bind events JW -> WIREWAX
this.player.on("play", this.JWPlayHandler);
this.player.on("pause", this.JWPauseHandler);
this.player.on("seek", this.JWSeekHandler);
// Bind events WIREWAX -> JW
this.embedder.on("play", this.WirewaxPlayHandler);
this.embedder.on("pause", this.WirewaxPauseHandler);
this.embedder.on("seeked", this.WirewaxSeekedHandler);
// Interaction event handlers
this.embedder.on("overlayshow", this.WirewaxOverlayShowHandler);
this.embedder.on("overlayhide", this.WirewaxOverlayHideHandler);
this.embedder.on("hotspotclick", this.WirewaxHotspotClickHandler);
this.isPlayerRegistered = true;
}
// ES6
JWPlayHandler = () => {
this.startTimeUpdate();
try {
this.embedder.play();
// custom tracking for play goes here
// ...
} catch (error) {
console.warn(error);
}
};
JWPauseHandler = () => {
this.stopTimeUpdate();
try {
this.embedder.pause();
// custom tracking for pause goes here
// ...
} catch (error) {
console.warn(error);
}
};
JWSeekHandler = (event) => {
try {
this.embedder.setCurrentTime(event.offset);
// custom tracking for seek goes here
// ...
} catch (error) {
console.warn(error);
}
};
WirewaxPlayHandler = () => {
try {
this.player.play();
} catch (err) {
console.log(err);
}
};
WirewaxPauseHandler = () => {
try {
this.player.pause();
} catch (err) {
console.log(err);
}
};
WirewaxSeekedHandler = ({ seekTo }) => {
try {
this.player.seek(seekTo);
} catch (err) {
console.log(err);
}
};
WirewaxHotspotClickHandler = (event) => {
// custom tracking for hotspot click goes here
// ...
};
WirewaxOverlayShowHandler = (event) => {
// custom tracking for overlay show goes here
// ...
};
WirewaxOverlayHideHandler = (event) => {
// custom tracking for overlay hide goes here
// ...
};
}

Additional methods#

On playlist item change, sometimes the interaction from the previous video will persist. We've provided a dispose method to dispose the WIREWAX player.

// fandom-jw-plugin.js
this.player.on("playlistItem", () => {
if (this.embedder) {
this.embedder.dispose(); // Dispose method here only dispose WIREWAX video-less player, not embedder itself.
}
}

Full template#

// fandom-jw-plugin.js
console.log("FandomPlugin loaded");
// Utilities
const fetchWIREWAXVidId = async (mediaid) => {
if (!mediaid) {
throw new TypeError("No JW media id is specified.");
}
const response = await fetch(
`https://edge-player.wirewax.com/jwPlayerData/${mediaid}.txt`
);
if (response.status !== 200) {
throw new Error("No vidId is mapped with this mediaid");
}
const vidId = await response.json();
return vidId;
};
const injectEmbedderSDK = () => {
if (window.createWirewaxEmbedder) {
console.warn("Embedder SDK is already loaded");
return;
}
const fandomSDKUrl =
"https://edge-assets-wirewax.wikia-services.com/creativeData/sdk-fandom/wirewax-embedder-sdk.js";
console.log("inject WIREWAX embedder SDK fandom build", fandomSDKUrl);
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = fandomSDKUrl;
script.addEventListener("load", resolve);
script.addEventListener("error", (e) => reject(e.error));
document.head.appendChild(script);
});
};
class FandomPlugin {
constructor(rootId, options) {
this.isPlayerRegistered = false;
this.rootId = rootId;
this.options = options;
this.player = options.player;
this.autoPlay = true;
this.player.on("playlistItem", () => {
if (this.embedder) {
this.stopTimeUpdate();
// Dispose pre video interaction
try {
this.embedder.dispose();
} catch (error) {
console.log(error);
}
}
// Search JW media id
const mediaId =
this.player.getConfig().playlistItem.mediaid ||
this.player.getConfig().playlistItem.videoId;
// validate interaction
fetchWIREWAXVidId(mediaId)
.then((vidId) => {
this.vidId = vidId;
// Inject SDK
return injectEmbedderSDK();
})
.then(() => this.setupEmbedder())
.then(() => this.registerEvents())
.catch((error) => {
console.warn(error);
});
});
return this;
}
on(event, callback) {
// ...
}
async setupEmbedder() {
if (!this.embedder) {
// Create a container
this.container = document.createElement("div");
this.container.classList.add("vjs-wirewax-container");
this.container.setAttribute(
"style",
"position: absolute; height: 100%; width: 100%; top: 0; pointer-events: none"
);
this.player.getContainer().appendChild(this.container);
// Initialize embedder
this.embedder = window.createWirewaxEmbedder();
}
// Create video-less WIREWAX player
this.embedder.createEl(this.container, {
isPlugin: true,
videoId: this.vidId,
rootId: this.rootId,
});
return this.embedder.ready();
}
registerEvents() {
// Custom time sync handler
const HTML5VideoEl = this.player.getConfig().mediaElement;
this.setWIREWAXCurrentTime = () => {
this.embedder.setCurrentTime(HTML5VideoEl.currentTime);
this.animationId = window.requestAnimationFrame(
this.setWIREWAXCurrentTime
);
};
// Handle the delay caused by injecting script
const isPlaying = this.player.getState() === "playing";
if (isPlaying || this.autoPlay) {
this.startTimeUpdate();
this.embedder.play();
}
if (this.isPlayerRegistered) return;
// Bind WIREWAX to JW player events
this.player.on("play", this.JWPlayHandler);
this.player.on("pause", this.JWPauseHandler);
this.player.on("seek", this.JWSeekHandler);
// Bind JW to WIREWAX events
this.embedder.on("play", this.WirewaxPlayHandler);
this.embedder.on("pause", this.WirewaxPauseHandler);
this.embedder.on("seeked", this.WirewaxSeekedHandler);
this.embedder.on("overlayshow", this.WirewaxOverlayShowHandler);
this.embedder.on("overlayhide", this.WirewaxOverlayHideHandler);
this.embedder.on("hotspotclick", this.WirewaxHotspotClickHandler);
this.isPlayerRegistered = true;
}
startTimeUpdate() {
window.cancelAnimationFrame(this.animationId);
this.animationId = window.requestAnimationFrame(this.setWIREWAXCurrentTime);
}
stopTimeUpdate() {
window.cancelAnimationFrame(this.animationId);
}
// ES6
JWPlayHandler = () => {
console.log("JW -> WIREWAX: play");
this.startTimeUpdate();
try {
this.embedder.play();
} catch (error) {
console.warn(error);
}
};
JWPauseHandler = () => {
console.log("JW -> WIREWAX: pause");
this.stopTimeUpdate();
try {
this.embedder.pause();
} catch (error) {
console.warn(error);
}
};
JWSeekHandler = (event) => {
console.log("JW -> WIREWAX: seek");
try {
this.embedder.setCurrentTime(event.offset);
} catch (error) {
console.warn(error);
}
};
WirewaxPlayHandler = () => {
console.log("WIREWAX -> JW: play");
try {
this.player.play();
} catch (err) {
console.log(err);
}
};
WirewaxPauseHandler = () => {
console.log("WIREWAX -> JW: pause");
try {
this.player.pause();
} catch (err) {
console.log(err);
}
};
WirewaxSeekedHandler = ({ seekTo }) => {
console.log("WIREWAX -> JW: seek");
try {
this.player.seek(seekTo);
} catch (err) {
console.log(err);
}
};
WirewaxHotspotClickHandler = (event) => {
console.log("hotspot click", { event });
};
WirewaxOverlayShowHandler = (event) => {
console.log("overlay open", { event });
};
WirewaxOverlayHideHandler = (event) => {
console.log("overlay close", { event });
};
}
const registerPlugin =
window.jwplayerPluginJsonp || window.jwplayer().registerPlugin;
registerPlugin("wirewax", "8.0", FandomPlugin);
export default FandomPlugin;