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