> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://mediakind.ferndocs.com/mkio/reference/player-sdk/walkthrough/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://mediakind.ferndocs.com/_mcp/server.
# End-to-end walkthrough
Build an MKPlayer integration from a blank HTML file to a working player that loads DRM-protected content, displays subtitles, and handles errors. Complete the sections in order because each one builds on the previous result.
By the end you will have a self-contained page you can adapt for your own project.
## What this covers
* Installing MKPlayer and wiring up the HTML
* Initialising the player with a full configuration
* Loading DRM-protected content with Widevine, PlayReady, and FairPlay
* Enabling subtitles and styling them at runtime
* Subscribing to events and surfacing errors to the user
* Cleaning up the player correctly when done
## Prerequisites
* npm available in your project
* A player license key from MediaKind
* Your deployment domain added to the MediaKind license allowlist
* A Widevine and/or PlayReady license server URL for your content
## The HTML page
Start with a minimal HTML page. Include the MKPlayer script and stylesheet in `
`, and add a container `
` and a few controls in ``:
```html
MKPlayer Example
```
The `status` paragraph is where you will surface error messages to the user. The actual player logic lives in `player.js`.
## Step 1: Initialise the player
In `player.js`, wait for the DOM to be ready, then create an `MKPlayer` instance.
The player configuration sets your license key, enables the built-in UI, and registers event handlers inline. Subscribing to `Error` and `Playing` here ensures you catch events that fire early in the player lifecycle, before you have a chance to call `player.on()` after construction.
player.js
```javascript
const statusEl = document.getElementById("status");
function setStatus(message) {
statusEl.textContent = message;
}
// Wait for the DOM to be ready before accessing elements
window.addEventListener("load", () => {
const container = document.getElementById("player-container");
const playerConfig = {
key: "YOUR_PLAYER_LICENSE_KEY",
ui: true,
playback: {
// muted is required for autoplay to work in most browsers
muted: false,
autoplay: false,
// Prevent the video going fullscreen automatically on iOS
playsInline: true
},
events: {
[mkplayer.MKPlayerEvent.Playing]: () => {
setStatus("");
},
[mkplayer.MKPlayerEvent.Error]: (event) => {
setStatus(`Error ${event.code}: ${event.message}`);
console.error("MKPlayer error", event);
},
[mkplayer.MKPlayerEvent.StallStarted]: () => {
setStatus("Buffering...");
},
[mkplayer.MKPlayerEvent.StallEnded]: () => {
setStatus("");
}
}
};
const player = new mkplayer.MKPlayer(container, playerConfig);
loadContent(player);
});
```
Unmuted autoplay is blocked by most browsers unless the user has previously interacted with the page. If you need autoplay, set `muted: true`. The user can then unmute using the player controls.
## Step 2: Load DRM-protected content
Define `loadContent()` to build a source configuration and call `player.load()`. The `drm` block covers Widevine and PlayReady for Chrome, Firefox, and Edge, plus FairPlay for Safari on Apple devices. The player selects the first key system supported by the current browser.
```javascript
function loadContent(player) {
const sourceConfig = {
title: "My Protected Stream",
description: "An example of DRM-protected DASH and HLS playback",
poster: "https://my-cdn.com/content/poster.jpg",
// Provide both DASH and HLS. The player picks based on platform support
dash: "https://my-cdn.com/content/dash/manifest.mpd",
hls: "https://my-cdn.com/content/hls/index.m3u8",
drm: {
widevine: {
LA_URL: "https://your-widevine-license-server/license",
headers: {
"X-DRM-Token": "your-token-value"
}
},
playready: {
LA_URL: "https://your-playready-license-server/license",
utf8message: true,
plaintextChallenge: true,
headers: {
"Content-Type": "text/xml",
"X-DRM-Token": "your-token-value"
}
},
// FairPlay applies only to HLS sources on Apple devices
fairplay: {
LA_URL: "https://your-fairplay-license-server/license",
certificateURL: "https://your-fairplay-certificate-url",
headers: {
"X-DRM-Token": "your-token-value"
}
}
},
subtitleTracks: [
{
id: "en-subs",
label: "English",
lang: "en",
kind: "subtitle",
url: "https://my-cdn.com/content/subtitles/en.vtt"
},
{
id: "fr-subs",
label: "French",
lang: "fr",
kind: "subtitle",
url: "https://my-cdn.com/content/subtitles/fr.vtt"
}
]
};
setStatus("Loading...");
player.load(sourceConfig)
.then(() => {
console.log("Source loaded successfully");
setupSubtitles(player);
})
.catch((error) => {
setStatus(`Failed to load source: ${error.message}`);
console.error("Load failed", error);
});
}
```
## Step 3: Enable subtitles
Once the source has loaded, list the available subtitle tracks and enable one by default. You can also let the player select a preferred language by setting `subtitleLanguage` in the `playback` configuration. This example selects the track explicitly so you can use its ID in the next step.
```javascript
function setupSubtitles(player) {
const tracks = player.subtitles.list();
if (tracks.length === 0) {
return;
}
// Log available tracks for debugging
console.log("Available subtitle tracks:", tracks.map(t => `${t.id} (${t.lang})`));
// Enable English subtitles by default if available
const englishTrack = tracks.find(t => t.lang === "en");
if (englishTrack) {
player.subtitles.enable(englishTrack.id);
applySubtitleStyle(player);
}
}
function applySubtitleStyle(player) {
player.setSubtitleStyle({
fontSize: "50",
fontStyle: "normal",
fontFamily: "normal",
fontColor: "white",
fontOpacity: "100",
backgroundColor: "black"
});
}
```
## Step 4: Add remaining event handling
Add event listeners after the player is created to handle the remaining playback lifecycle. This example registers them dynamically with `player.on()` instead of adding them to the configuration:
```javascript
function attachEvents(player) {
player.on(mkplayer.MKPlayerEvent.SourceLoaded, () => {
console.log("Source loaded");
});
player.on(mkplayer.MKPlayerEvent.PlaybackFinished, () => {
console.log("Playback finished");
setStatus("Playback complete.");
});
player.on(mkplayer.MKPlayerEvent.Destroy, () => {
console.log("Player destroyed");
});
}
```
Call `attachEvents(player)` immediately after creating the player instance, before `loadContent()`.
## Step 5: Clean up
When the page unloads, or when you are done with the player, unload the source and destroy the instance to release all held resources:
```javascript
window.addEventListener("beforeunload", () => {
if (player) {
player.unload()
.then(() => player.destroy())
.catch(() => player.destroy());
}
});
```
Do not call any player API methods after `destroy()`. The player will throw a `PLAYER_API_NOT_AVAILABLE` error.
## Complete player.js
Putting it all together:
player.js
```javascript
const statusEl = document.getElementById("status");
function setStatus(message) {
statusEl.textContent = message;
}
function applySubtitleStyle(player) {
player.setSubtitleStyle({
fontSize: "50",
fontStyle: "normal",
fontFamily: "normal",
fontColor: "white",
fontOpacity: "100",
backgroundColor: "black"
});
}
function setupSubtitles(player) {
const tracks = player.subtitles.list();
if (tracks.length === 0) return;
const englishTrack = tracks.find(t => t.lang === "en");
if (englishTrack) {
player.subtitles.enable(englishTrack.id);
applySubtitleStyle(player);
}
}
function attachEvents(player) {
player.on(mkplayer.MKPlayerEvent.SourceLoaded, () => {
console.log("Source loaded");
});
player.on(mkplayer.MKPlayerEvent.PlaybackFinished, () => {
setStatus("Playback complete.");
});
player.on(mkplayer.MKPlayerEvent.Destroy, () => {
console.log("Player destroyed");
});
}
function loadContent(player) {
const sourceConfig = {
title: "My Protected Stream",
description: "An example of DRM-protected DASH and HLS playback",
poster: "https://my-cdn.com/content/poster.jpg",
dash: "https://my-cdn.com/content/dash/manifest.mpd",
hls: "https://my-cdn.com/content/hls/index.m3u8",
drm: {
widevine: {
LA_URL: "https://your-widevine-license-server/license",
headers: {
"X-DRM-Token": "your-token-value"
}
},
playready: {
LA_URL: "https://your-playready-license-server/license",
utf8message: true,
plaintextChallenge: true,
headers: {
"Content-Type": "text/xml",
"X-DRM-Token": "your-token-value"
}
},
fairplay: {
LA_URL: "https://your-fairplay-license-server/license",
certificateURL: "https://your-fairplay-certificate-url",
headers: {
"X-DRM-Token": "your-token-value"
}
}
},
subtitleTracks: [
{
id: "en-subs",
label: "English",
lang: "en",
kind: "subtitle",
url: "https://my-cdn.com/content/subtitles/en.vtt"
},
{
id: "fr-subs",
label: "French",
lang: "fr",
kind: "subtitle",
url: "https://my-cdn.com/content/subtitles/fr.vtt"
}
]
};
setStatus("Loading...");
player.load(sourceConfig)
.then(() => setupSubtitles(player))
.catch((error) => {
setStatus(`Failed to load source: ${error.message}`);
console.error("Load failed", error);
});
}
window.addEventListener("load", () => {
const container = document.getElementById("player-container");
const playerConfig = {
key: "YOUR_PLAYER_LICENSE_KEY",
ui: true,
playback: {
muted: false,
autoplay: false,
playsInline: true
},
events: {
[mkplayer.MKPlayerEvent.Playing]: () => setStatus(""),
[mkplayer.MKPlayerEvent.Error]: (event) => {
setStatus(`Error ${event.code}: ${event.message}`);
console.error("MKPlayer error", event);
},
[mkplayer.MKPlayerEvent.StallStarted]: () => setStatus("Buffering..."),
[mkplayer.MKPlayerEvent.StallEnded]: () => setStatus("")
}
};
const player = new mkplayer.MKPlayer(container, playerConfig);
attachEvents(player);
loadContent(player);
window.addEventListener("beforeunload", () => {
player.unload()
.then(() => player.destroy())
.catch(() => player.destroy());
});
});
```
## What to replace
Before running this, replace the following placeholder values with your own:
| Placeholder | Replace with |
| ----------------------------------------------- | ------------------------------------------------------ |
| `YOUR_PLAYER_LICENSE_KEY` | Your player license key from MediaKind |
| `https://your-widevine-license-server/license` | Your Widevine license server URL |
| `https://your-playready-license-server/license` | Your PlayReady license server URL |
| `https://your-fairplay-license-server/license` | Your FairPlay license server URL |
| `https://your-fairplay-certificate-url` | Your FairPlay certificate URL |
| `your-token-value` | Your DRM auth token or other required header values |
| All `https://my-cdn.com/...` URLs | Your actual stream manifest, poster, and subtitle URLs |
## Common issues at this stage
**The player throws `PLAYER_SETUP_MISSING_LICENSE_ALLOWLIST`.**
Your domain is not on the MediaKind license allowlist. Contact MediaKind to add it before testing.
**The player throws `PLAYER_DRM_FAILED_LICENSE_REQUEST`.**
The license server rejected the request. Check that your `LA_URL` is correct, that any required auth headers are present, and that the token has not expired.
**Subtitles do not appear after `player.subtitles.enable()`.**
Check that `enableSubtitleOverlay` is not set to `false` in your player configuration and that `ui` is set to `false`. The built-in subtitle overlay is disabled when `ui` is enabled. If you are using `ui: true`, the UI layer handles subtitle rendering.
**Safari does not play back the stream.**
Ensure you have placed `serviceWorker.js` at the root of your domain and set `tweaks: { native_hls_parsing: true }` in your player config. See [Installation and setup](/mkio/reference/player-sdk/setup) for details.