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.
What this covers
Section titled “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
Section titled “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
Section titled “The HTML page”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.
Step 1: Initialise the player
Section titled “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.
const statusEl = document.getElementById("status");
function setStatus(message) { statusEl.textContent = message;}
// Wait for the DOM to be ready before accessing elementswindow.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
Section titled “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.
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
Section titled “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 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" });}Step 4: Add remaining event handling
Section titled “Step 4: Add remaining event handling”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().
Step 5: Clean up
Section titled “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:
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
Section titled “Complete player.js”Putting it all together:
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
Section titled “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
Section titled “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 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.