# 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 `<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

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

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

```
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:

```
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:

```
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:

```
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"

}

}

},

};

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](https://docs.mediakind.com/mkio/reference/player-sdk/setup) for details.
