Skip to content

End-to-end walkthrough

This page walks through a complete MKPlayer integration from a blank HTML file to a working player that loads DRM-protected content, displays subtitles, and handles errors. Each section builds on the last.

By the end you will have a self-contained page you can adapt for your own project.

  • 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
  • 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

Start with a minimal HTML page. Include the MKPlayer script and stylesheet in <head>, and add a container <div> and a few controls in <body>:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MKPlayer Example</title>
<script type="text/javascript" src="./node_modules/@mediakind/mkplayer/mkplayer.js"></script>
<link rel="stylesheet" href="./node_modules/@mediakind/mkplayer/mkplayer-ui.css">
<style>
#player-container { width: 100%; max-width: 960px; aspect-ratio: 16/9; background: #000; }
#status { margin-top: 8px; font-family: monospace; font-size: 14px; color: #c00; min-height: 20px; }
</style>
</head>
<body>
<div id="player-container"></div>
<p id="status"></p>
<script src="./player.js"></script>
</body>
</html>

The status paragraph is where you will surface error messages to the user. The actual player logic lives in player.js.

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
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.

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.

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);
});
}

Once the source has loaded, list the available subtitle tracks and enable one by default. You can also let the player auto-select based on a preferred language by setting subtitleLanguage in playback config — here you do it explicitly so you have a reference to the track ID for the style change in the next step.

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"
});
}

Add a few more event listeners after the player is created to handle the full playback lifecycle. These are registered dynamically with player.on() rather than in the config — both approaches work, and this keeps the config lean:

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().

When the page unloads, or when you are done with the player, unload the source and destroy the instance to release all held resources:

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.

Putting it all together:

player.js
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());
});
});

Before running this, replace the following placeholder values with your own:

PlaceholderReplace with
YOUR_PLAYER_LICENSE_KEYYour player license key from MediaKind
https://your-widevine-license-server/licenseYour Widevine license server URL
https://your-playready-license-server/licenseYour PlayReady license server URL
https://your-fairplay-license-server/licenseYour FairPlay license server URL
https://your-fairplay-certificate-urlYour FairPlay certificate URL
your-token-valueYour DRM auth token or other required header values
All https://my-cdn.com/... URLsYour actual stream manifest, poster, and subtitle URLs

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 config, and that ui is set to false — the built-in subtitle overlay is disabled when ui is enabled. If you are using ui: true, subtitle rendering is handled by the UI layer directly.

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 for details.

© 2026 MediaKind. All rights reserved.